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.members;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Objects;
027import java.util.Optional;
028import java.util.Set;
029import java.util.function.Predicate;
030import java.util.stream.Collectors;
031
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.context.Context;
034import org.apache.avalon.framework.context.ContextException;
035import org.apache.avalon.framework.context.Contextualizable;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.avalon.framework.service.Serviceable;
039import org.apache.cocoon.components.ContextHelper;
040import org.apache.cocoon.environment.Request;
041import org.apache.commons.lang3.StringUtils;
042
043import org.ametys.core.group.Group;
044import org.ametys.core.group.GroupIdentity;
045import org.ametys.core.group.GroupManager;
046import org.ametys.core.observation.Event;
047import org.ametys.core.observation.ObservationManager;
048import org.ametys.core.right.Profile;
049import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint;
050import org.ametys.core.right.RightManager;
051import org.ametys.core.right.RightProfilesDAO;
052import org.ametys.core.ui.Callable;
053import org.ametys.core.user.CurrentUserProvider;
054import org.ametys.core.user.User;
055import org.ametys.core.user.UserIdentity;
056import org.ametys.core.user.UserManager;
057import org.ametys.plugins.core.user.UserHelper;
058import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
059import org.ametys.plugins.repository.AmetysObject;
060import org.ametys.plugins.repository.AmetysObjectIterable;
061import org.ametys.plugins.repository.AmetysObjectResolver;
062import org.ametys.plugins.repository.AmetysRepositoryException;
063import org.ametys.plugins.repository.ModifiableAmetysObject;
064import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
065import org.ametys.plugins.repository.RepositoryConstants;
066import org.ametys.plugins.workspaces.ObservationConstants;
067import org.ametys.plugins.workspaces.members.JCRProjectMember.MemberType;
068import org.ametys.plugins.workspaces.project.ProjectManager;
069import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
070import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
071import org.ametys.plugins.workspaces.project.objects.Project;
072import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus;
073import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper;
074import org.ametys.runtime.plugin.component.AbstractLogEnabled;
075import org.ametys.web.population.PopulationContextHelper;
076import org.ametys.web.repository.page.ModifiablePage;
077import org.ametys.web.repository.page.Page;
078import org.ametys.web.repository.site.Site;
079
080import com.google.common.collect.ImmutableList;
081
082/**
083 * Helper component for managing project's users
084 */
085public class ProjectMemberManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
086{
087    /** Avalon Role */
088    public static final String ROLE = ProjectMemberManager.class.getName();
089    
090    /** The id of the members service */
091    public static final String __WORKSPACES_SERVICE_MEMBERS = "org.ametys.plugins.workspaces.module.Members";
092    
093    /** Constants for users project node */
094    private static final String __PROJECT_MEMBERS_NODE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":members";
095    
096    /** The type of the project users node type */
097    private static final String __PROJECT_MEMBERS_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured";
098
099    /** The type of a project user node type */
100    private static final String __PROJECT_MEMBER_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project-member";
101 
102    /** Project Right to add a member to a project */
103    private static final String __RIGHTS_ADD_MEMBER_TO_PROJECT = "Plugins_Workspaces_Rights_Service_Module_Members_Add";
104    
105    /** Project Right to remove a member from the project */
106    private static final String __RIGHTS_REMOVE_MEMBER_TO_PROJECT = "Plugins_Workspaces_Rights_Service_Module_Members_Remove";
107    
108    private static final String __PROJECT_RIGHT_PROFILE = "PROJECT";
109    
110    /** Avalon context */
111    protected Context _context;
112    
113    /** Project manager */
114    protected ProjectManager _projectManager;
115
116    /** Project rights helper */
117    protected ProjectRightHelper _projectRightHelper;
118
119    /** Profiles right manager */
120    protected RightProfilesDAO _rightProfilesDAO;
121
122    /** Profile assignment storage */
123    protected ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageExtensionPoint;
124    
125    /** Ametys object resolver */
126    protected AmetysObjectResolver _resolver;
127
128    /** Rights manager */
129    protected RightManager _rightManager;
130
131    /** Current user provider */
132    protected CurrentUserProvider _currentUserProvider;
133
134    /** Users manager */
135    protected UserManager _userManager;
136
137    /** The observation manager */
138    protected ObservationManager _observationManager;
139
140    /** Module managers EP */
141    protected WorkspaceModuleExtensionPoint _moduleManagerEP;
142    
143    /** The user helper */
144    protected UserHelper _userHelper;
145
146    /** The groups manager */
147    protected GroupManager _groupManager;
148
149    private PopulationContextHelper _populationContextHelper;
150    
151    @Override
152    public void contextualize(Context context) throws ContextException
153    {
154        _context = context;
155    }
156    
157    @Override
158    public void service(ServiceManager manager) throws ServiceException
159    {
160        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
161        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
162        _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE);
163        _rightProfilesDAO = (RightProfilesDAO) manager.lookup(RightProfilesDAO.ROLE);
164        _profileAssignmentStorageExtensionPoint = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE);
165        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
166        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
167        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
168        _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE);
169        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
170        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
171        _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
172        _populationContextHelper = (PopulationContextHelper) manager.lookup(org.ametys.core.user.population.PopulationContextHelper.ROLE);
173    }
174    
175    /**
176     * Retrieve the data of a member of a project, or the default data if no user is provided
177     * @param projectName The name of the project
178     * @param identity The user or group identity. If null, return the default profiles for a new user
179     * @param type The type of the identity. Can be "user" or "group"
180     * @return The map of profiles per module for the user
181     * @throws IllegalAccessException If the user cannot execute this operation 
182     */
183    @Callable
184    public Map<String, Object> getProjectMemberData(String projectName, String identity, String type) throws IllegalAccessException
185    {
186        Map<String, Object> result = new HashMap<>();
187        
188        boolean isTypeUser = JCRProjectMember.MemberType.USER.toString().equals(type);
189        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.toString().equals(type);
190        UserIdentity user = Optional.ofNullable(identity)
191                                    .filter(id -> id != null && isTypeUser)
192                                    .map(UserIdentity::stringToUserIdentity)
193                                    .orElse(null);
194        GroupIdentity group = Optional.ofNullable(identity)
195                                      .filter(id -> id != null && isTypeGroup)
196                                      .map(GroupIdentity::stringToGroupIdentity)
197                                      .orElse(null);
198        
199        if (identity != null)
200        {
201            if (isTypeGroup && group == null)
202            {
203                result.put("message", "unknow-group");
204                return result;
205            }
206            else if (isTypeUser && user == null)
207            {
208                result.put("message", "unknow-user");
209                return result;
210            }
211        }
212        
213        Project project = _projectManager.getProject(projectName);
214        if (project == null)
215        {
216            result.put("message", "unknow-project");
217            return result;
218        }
219        
220        if (!_projectRightHelper.hasRight(__RIGHTS_ADD_MEMBER_TO_PROJECT, project))
221        {
222            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without convenient right [" + __RIGHTS_ADD_MEMBER_TO_PROJECT + ", /projects" + project.getPath() + "]");
223        }
224        
225        boolean newMember = true;
226        Map<String, String> userProfiles;
227        
228        if (user != null || group != null)
229        {
230            JCRProjectMember projectMember = user != null ? getOrCreateProjectMember(project, user) : getOrCreateProjectMember(project, group);
231            
232            newMember = projectMember.needsSave();
233            
234            String role = projectMember.getRole();
235            if (role != null)
236            {
237                result.put("role", role);
238            }
239            
240            userProfiles = _getMemberProfiles(projectMember, project);
241        }
242        else
243        {
244            userProfiles = new HashMap<>();
245        }
246        
247        result.put("profiles", userProfiles);
248        result.put("status", newMember ? "new" : "edit");
249        
250        return result;
251    }
252
253    private Map<String, String> _getMemberProfiles(JCRProjectMember member, Project project)
254    {
255        Map<String, String> userProfiles = new HashMap<>();
256        
257        List<String> profileIds = _projectRightHelper.getProfileList()
258                                                     .stream()
259                                                     .map(p -> p.getId())
260                                                     .collect(Collectors.toList());
261        
262        // Get allowed profile on modules (among the project members's profiles)
263        for (WorkspaceModule module : _projectManager.getModules(project))
264        {
265            AmetysObject moduleObject = module.getModuleRoot(project, false);
266            Set<String> allowedProfilesForUser = MemberType.GROUP.toString().equals(member.getType())
267                    ? _profileAssignmentStorageExtensionPoint.getAllowedProfilesForGroup(moduleObject, member.getGroup())
268                    : _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(moduleObject, member.getUser());
269            
270            for (String allowedProfile : allowedProfilesForUser)
271            {
272                if (profileIds.contains(allowedProfile))
273                {
274                    // Get the first allowed profile among the project's members profiles
275                    userProfiles.put(module.getId(), allowedProfile);
276                    break;
277                }
278            }
279            
280            if (!userProfiles.containsKey(module.getId()))
281            {
282                userProfiles.put(module.getId(), null);
283            }
284        }
285        
286        // Get allowed profile on project (among the project members's profiles)
287        Set<String> allowedProfilesOnProject = MemberType.GROUP.toString().equals(member.getType()) 
288                ? _profileAssignmentStorageExtensionPoint.getAllowedProfilesForGroup(project, member.getGroup()) 
289                : _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(project, member.getUser());
290        for (String allowedProfile : allowedProfilesOnProject)
291        {
292            if (profileIds.contains(allowedProfile))
293            {
294                // Get the first allowed profile among the project's members profiles
295                userProfiles.put(__PROJECT_RIGHT_PROFILE, allowedProfile);
296                break;
297            }
298        }
299        
300        if (!userProfiles.containsKey(__PROJECT_RIGHT_PROFILE))
301        {
302            userProfiles.put(__PROJECT_RIGHT_PROFILE, null);
303        }
304        return userProfiles;
305    }
306    
307    /**
308     * Set the user data in the project
309     * @param projectName The project name
310     * @param identity The user or group identity.
311     * @param type The type of the identity. Can be "user" or "group"
312     * @param newProfiles The profiles to affect, mapped by module
313     * @param role The user role inside the project
314     * @return The result
315     * @throws IllegalAccessException If the user cannot execute this operation
316     */
317    @Callable
318    public Map<String, Object> setProjectMemberData(String projectName, String identity, String type, Map<String, String> newProfiles, String role) throws IllegalAccessException
319    {
320        Map<String, Object> result = new HashMap<>();
321        
322        boolean isTypeUser = JCRProjectMember.MemberType.USER.toString().equals(type);
323        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.toString().equals(type);
324        UserIdentity user = Optional.ofNullable(identity)
325                                    .filter(id -> id != null && isTypeUser)
326                                    .map(UserIdentity::stringToUserIdentity)
327                                    .orElse(null);
328        GroupIdentity group = Optional.ofNullable(identity)
329                                      .filter(id -> id != null && isTypeGroup)
330                                      .map(GroupIdentity::stringToGroupIdentity)
331                                      .orElse(null);
332        
333        if (group == null && user == null)
334        {
335            result.put("message", isTypeGroup ? "unknow-group" : "unknow-user");
336            return result;
337        }
338        
339        
340        Project project = _projectManager.getProject(projectName);
341        if (project == null)
342        {
343            result.put("message", "unknow-project");
344            return result;
345        }
346        
347        if (!_projectRightHelper.hasRight(__RIGHTS_ADD_MEMBER_TO_PROJECT, project))
348        {
349            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without convenient right [" + __RIGHTS_ADD_MEMBER_TO_PROJECT + ", /projects" + project.getPath() + "]");
350        }
351        
352        JCRProjectMember projectMember = isTypeUser ? getOrCreateProjectMember(project, user) : getOrCreateProjectMember(project, group);
353        boolean newMember = projectMember.needsSave();
354        
355        if (role != null)
356        {
357            projectMember.setRole(role);
358        }
359        
360        _setMemberProfiles(newProfiles, projectMember, project, newMember);
361        
362        project.saveChanges();
363        
364        if (newMember)
365        {
366            // Notify listeners
367            Map<String, Object> eventParams = new HashMap<>();
368            eventParams.put(ObservationConstants.ARGS_MEMBER, projectMember);
369            eventParams.put(ObservationConstants.ARGS_MEMBER_ID, projectMember.getId());
370            eventParams.put(ObservationConstants.ARGS_PROJECT, project);
371            eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
372            _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_ADDED, _currentUserProvider.getUser(), eventParams));
373        }
374        
375        return result;
376    }
377    
378    /**
379     * Add a user to a project with open inscriptions, using the default values
380     * @param project The project
381     * @param user The user
382     * @return True if the user was successfully added
383     */
384    public boolean addProjectMember(Project project, UserIdentity user)
385    {
386        if (user == null)
387        {
388            return false;
389        }
390        
391        InscriptionStatus inscriptionStatus = project.getInscriptionStatus();
392        if (!inscriptionStatus.equals(InscriptionStatus.OPEN))
393        {
394            return false;
395        }
396        
397        JCRProjectMember projectMember = getOrCreateProjectMember(project, user);
398        
399        // Allow the user to access the project's site
400        _allowReadAccessOnProjectDashboard(projectMember, project);
401        List<Profile> projectProfiles = _projectRightHelper.getProfileList();
402        List<String> allowedProfiles = new ArrayList<>();
403        String defaultProfile = project.getDefaultProfile();
404        if (defaultProfile != null)
405        {
406            allowedProfiles.add(defaultProfile);
407        }
408        allowedProfiles.add(RightManager.READER_PROFILE_ID);
409        _setMemberProfiles(projectMember, projectProfiles, allowedProfiles, project);
410        
411        for (WorkspaceModule module : _moduleManagerEP.getModules())
412        {
413            if (module != null && _projectManager.isModuleActivated(project, module.getId()))
414            {
415                AmetysObject moduleObject = module.getModuleRoot(project, false);
416                _setMemberProfiles(projectMember, projectProfiles, allowedProfiles, moduleObject);
417                
418                // Update read access on project module's pages
419                AmetysObjectIterable<Page> modulePages = module.getModulePages(project, null);
420                for (Page modulePage : modulePages)
421                {
422                    _updateReadAccessOnModulePage(projectMember, modulePage, allowedProfiles.size() > 0);
423                }
424            }
425        }
426        
427        project.saveChanges();
428        
429        // Notify listeners
430        Map<String, Object> eventParams = new HashMap<>();
431        eventParams.put(ObservationConstants.ARGS_MEMBER, projectMember);
432        eventParams.put(ObservationConstants.ARGS_MEMBER_ID, projectMember.getId());
433        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
434        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
435        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_ADDED, _currentUserProvider.getUser(), eventParams));
436        
437        return true;
438    }
439
440    private void _setMemberProfiles(Map<String, String> newProfiles, JCRProjectMember projectMember, Project project, boolean newMember)
441    {
442        // Allow the user to access the project's site
443        _allowReadAccessOnProjectDashboard(projectMember, project);
444        
445        List<Profile> projectProfiles = _projectRightHelper.getProfileList();
446        for (Map.Entry<String, String> entry : newProfiles.entrySet())
447        {
448            List<String> allowedProfiles = new ArrayList<>();
449            if (entry.getValue() != null)
450            {
451                allowedProfiles.add(entry.getValue());
452            }
453            
454            String moduleId = entry.getKey();
455            if (__PROJECT_RIGHT_PROFILE.equals(moduleId))
456            {
457                if (newMember)
458                {
459                    allowedProfiles.add(RightManager.READER_PROFILE_ID);
460                }
461                _setMemberProfiles(projectMember, projectProfiles, allowedProfiles, project);
462            }
463            else
464            {
465                WorkspaceModule module = _moduleManagerEP.getModule(moduleId);
466                if (module != null && _projectManager.isModuleActivated(project, moduleId))
467                {
468                    AmetysObject moduleObject = module.getModuleRoot(project, false);
469                    _setMemberProfiles(projectMember, projectProfiles, allowedProfiles, moduleObject);
470                    
471                    // Update read access on project module's pages
472                    AmetysObjectIterable<Page> modulePages = module.getModulePages(project, null);
473                    for (Page modulePage : modulePages)
474                    {
475                        _updateReadAccessOnModulePage(projectMember, modulePage, allowedProfiles.size() > 0);
476                    }
477                }
478            }
479        }
480    }
481
482    private void _setMemberProfiles(JCRProjectMember member, List<Profile> projectProfiles, List<String> allowedProfiles, AmetysObject object)
483    {
484        Set<String> updatedProfiles = new HashSet<>();
485        
486        Set<String> currentAllowedProfiles = MemberType.GROUP.toString().equals(member.getType()) 
487                ? _profileAssignmentStorageExtensionPoint.getAllowedProfilesForGroup(object, member.getGroup()) 
488                : _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(object, member.getUser());
489        
490        if (allowedProfiles.size() == 0)
491        {
492            _removeMemberProfiles(member, projectProfiles, object, updatedProfiles, currentAllowedProfiles);
493        }
494        else
495        {
496            _setMemberProfiles(member, projectProfiles, allowedProfiles, object, updatedProfiles, currentAllowedProfiles);
497        }
498        
499        if (updatedProfiles.size() > 0)
500        {
501            Map<String, Object> eventParams = new HashMap<>();
502            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, object);
503            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, object.getId());
504            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, updatedProfiles);
505            
506            _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
507        }
508    }
509
510    private void _setMemberProfiles(JCRProjectMember member, List<Profile> projectProfiles, List<String> allowedProfiles, AmetysObject object, Set<String> updatedProfiles, Set<String> currentAllowedProfiles)
511    {
512        // Reset the setted project's profiles
513        for (Profile profile : projectProfiles)
514        {
515            if (currentAllowedProfiles.contains(profile.getId()) && !allowedProfiles.contains(profile.getId()))
516            {
517                if (MemberType.GROUP.toString().equals(member.getType()))
518                {
519                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), profile.getId(), object);
520                }
521                else
522                {
523                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), profile.getId(), object);
524                }
525                updatedProfiles.add(profile.getId());
526            }
527        }
528        
529        for (String allowedProfile : allowedProfiles)
530        {
531            if (!currentAllowedProfiles.contains(allowedProfile))
532            {
533                // Set the allowed profile
534                if (MemberType.GROUP.toString().equals(member.getType()))
535                {
536                    _profileAssignmentStorageExtensionPoint.allowProfileToGroup(member.getGroup(), allowedProfile, object);
537                }
538                else
539                {
540                    _profileAssignmentStorageExtensionPoint.allowProfileToUser(member.getUser(), allowedProfile, object);
541                }
542                updatedProfiles.add(allowedProfile);
543            }
544        }
545        
546        if (!currentAllowedProfiles.contains(RightManager.READER_PROFILE_ID) && !allowedProfiles.contains(RightManager.READER_PROFILE_ID))
547        {
548            // Allow the READER profiles
549            if (MemberType.GROUP.toString().equals(member.getType()))
550            {
551                _profileAssignmentStorageExtensionPoint.allowProfileToGroup(member.getGroup(), RightManager.READER_PROFILE_ID, object);
552            }
553            else
554            {
555                _profileAssignmentStorageExtensionPoint.allowProfileToUser(member.getUser(), RightManager.READER_PROFILE_ID, object);
556            }
557            updatedProfiles.add(RightManager.READER_PROFILE_ID);
558        }
559    }
560
561    private void _removeMemberProfiles(JCRProjectMember member, List<Profile> projectProfiles, AmetysObject object, Set<String> updatedProfiles, Set<String> currentAllowedProfiles)
562    {
563        // Reset the setted project's profiles
564        for (Profile profile : projectProfiles)
565        {
566            if (currentAllowedProfiles.contains(profile.getId()))
567            {
568                if (MemberType.GROUP.toString().equals(member.getType()))
569                {
570                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), profile.getId(), object);
571                }
572                else
573                {
574                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), profile.getId(), object);
575                }
576                updatedProfiles.add(profile.getId());
577            }
578        }
579        
580        // Remove the READER profile
581        if (currentAllowedProfiles.contains(RightManager.READER_PROFILE_ID))
582        {
583            if (MemberType.GROUP.toString().equals(member.getType()))
584            {
585                _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), RightManager.READER_PROFILE_ID, object);
586            }
587            else
588            {
589                _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), RightManager.READER_PROFILE_ID, object);
590            }
591            updatedProfiles.add(RightManager.READER_PROFILE_ID);
592        }
593    }
594    
595    private void _allowReadAccessOnProjectDashboard(JCRProjectMember member, Project project)
596    {
597        // Add READER profile for all dashboards of project
598        for (ModifiablePage dashboardPage : _getProjectDashboardPages(project))
599        {
600            Set<String> currentAllowedProfiles = MemberType.GROUP.toString().equals(member.getType()) 
601                    ? _profileAssignmentStorageExtensionPoint.getAllowedProfilesForGroup(dashboardPage, member.getGroup()) 
602                    : _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(dashboardPage, member.getUser());
603            if (!currentAllowedProfiles.contains(RightManager.READER_PROFILE_ID))
604            {
605                if (MemberType.GROUP.toString().equals(member.getType()))
606                {
607                    _profileAssignmentStorageExtensionPoint.allowProfileToGroup(member.getGroup(), RightManager.READER_PROFILE_ID, dashboardPage);
608                }
609                else
610                {
611                    _profileAssignmentStorageExtensionPoint.allowProfileToUser(member.getUser(), RightManager.READER_PROFILE_ID, dashboardPage);
612                }
613                
614                dashboardPage.saveChanges();
615                
616                Map<String, Object> eventParams = new HashMap<>();
617                eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, dashboardPage);
618                eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, dashboardPage.getId());
619                eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, Collections.singleton(RightManager.READER_PROFILE_ID));
620                
621                _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, MemberType.GROUP.toString().equals(member.getType()) ? null : member.getUser(), eventParams));
622            }
623        }
624    }
625
626    private List<ModifiablePage> _getProjectDashboardPages(Project project)
627    {
628        List<ModifiablePage> dashboardPages = project.getSites()
629                                                     .stream()
630                                                     .map(Site::getSitemaps)
631                                                     .flatMap(AmetysObjectIterable::stream)
632                                                     .map(sitemap -> _projectManager.getProjectDashboardPage(project, sitemap.getSitemapName()))
633                                                     .flatMap(AmetysObjectIterable::stream)
634                                                     .filter(page -> page instanceof ModifiablePage)
635                                                     .map(ModifiablePage.class::cast)
636                                                     .collect(Collectors.toList());
637        return dashboardPages;
638    }
639    
640    private void _removeReadAccessOnProjectDashboard(JCRProjectMember member, Project project)
641    {
642        // Add READER profile for all dashboard pages of project
643        for (ModifiablePage dashboardPage : _getProjectDashboardPages(project))
644        {
645            Set<String> currentAllowedProfiles = MemberType.GROUP.toString().equals(member.getType()) 
646                    ? _profileAssignmentStorageExtensionPoint.getAllowedProfilesForGroup(dashboardPage, member.getGroup()) 
647                    : _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(dashboardPage, member.getUser());
648            if (currentAllowedProfiles.contains(RightManager.READER_PROFILE_ID))
649            {
650                if (MemberType.GROUP.toString().equals(member.getType()))
651                {
652                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), RightManager.READER_PROFILE_ID, dashboardPage);
653                }
654                else
655                {
656                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), RightManager.READER_PROFILE_ID, dashboardPage);
657                }
658                
659                dashboardPage.saveChanges();
660                
661                Map<String, Object> eventParams = new HashMap<>();
662                eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, dashboardPage);
663                eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, dashboardPage.getId());
664                eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, Collections.singleton(RightManager.READER_PROFILE_ID));
665                
666                _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
667            }
668        }
669    }
670    
671    private void _updateReadAccessOnModulePage(JCRProjectMember member, Page modulePage, boolean allowed)
672    {
673        Set<String> currentAllowedProfiles = MemberType.GROUP.toString().equals(member.getType()) 
674                ? _profileAssignmentStorageExtensionPoint.getAllowedProfilesForGroup(modulePage, member.getGroup()) 
675                : _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(modulePage, member.getUser());
676        
677        boolean hasChanges = false;
678        
679        if (allowed)
680        {
681            if (!currentAllowedProfiles.contains(RightManager.READER_PROFILE_ID))
682            {
683                if (MemberType.GROUP.toString().equals(member.getType()))
684                {
685                    _profileAssignmentStorageExtensionPoint.allowProfileToGroup(member.getGroup(), RightManager.READER_PROFILE_ID, modulePage);
686                }
687                else
688                {
689                    _profileAssignmentStorageExtensionPoint.allowProfileToUser(member.getUser(), RightManager.READER_PROFILE_ID, modulePage);
690                }
691                hasChanges = true;
692            }
693        }
694        else
695        {
696            // remove READER profile on module page
697            if (currentAllowedProfiles.contains(RightManager.READER_PROFILE_ID))
698            {
699                if (MemberType.GROUP.toString().equals(member.getType()))
700                {
701                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), RightManager.READER_PROFILE_ID, modulePage);
702                }
703                else
704                {
705                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), RightManager.READER_PROFILE_ID, modulePage);
706                }
707                hasChanges = true;
708            }
709        }
710        
711        if (hasChanges)
712        {
713            ((ModifiablePage) modulePage).saveChanges();
714            
715            Map<String, Object> eventParams = new HashMap<>();
716            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, modulePage);
717            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, modulePage.getId());
718            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, Collections.singleton(RightManager.READER_PROFILE_ID));
719            
720            _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
721        }
722    }
723    
724    private void _removeMemberProfiles(JCRProjectMember member, AmetysObject object)
725    {
726        Set<String> currentAllowedProfiles = MemberType.GROUP.toString().equals(member.getType()) 
727                ? _profileAssignmentStorageExtensionPoint.getAllowedProfilesForGroup(object, member.getGroup()) 
728                : _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(object, member.getUser());
729        
730        Set<String> removedProfiles = new HashSet<>();
731        
732        if (currentAllowedProfiles.size() > 0)
733        {
734            for (String allowedProfile : currentAllowedProfiles)
735            {
736                if (MemberType.GROUP.toString().equals(member.getType()))
737                {
738                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), allowedProfile, object);
739                }
740                else
741                {
742                    _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), allowedProfile, object);
743                }
744                removedProfiles.add(allowedProfile);
745            }
746        }
747        
748        if (removedProfiles.size() > 0)
749        {
750            ((ModifiableAmetysObject) object).saveChanges();
751            
752            Map<String, Object> eventParams = new HashMap<>();
753            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, object);
754            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, object.getId());
755            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, removedProfiles);
756            
757            _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
758        }
759    }
760    
761    /**
762     * Get the current user information
763     * @return The user
764     */
765    @Callable
766    public Map<String, Object> getCurrentUser()
767    {
768        Map<String, Object> result = new HashMap<>();
769        result.put("user", _userHelper.user2json(_currentUserProvider.getUser()));
770        return result;
771    }
772    
773    /**
774     * Get the members of current project or all the members of all projects in where is no current project
775     * @return The members
776     */
777    @Callable
778    public Map<String, Object> getProjectMembers()
779    {
780        Map<String, Object> result = new HashMap<>();
781        
782        Request request = ContextHelper.getRequest(_context);
783        String projectName = (String) request.getAttribute("projectName");
784        
785        Collection<Project> projects = new ArrayList<>();
786        
787        if (StringUtils.isNotEmpty(projectName))
788        {
789            projects.add(_projectManager.getProject(projectName));
790        }
791        else
792        {
793            _projectManager.getProjects()
794                           .stream()
795                           .forEach(project -> projects.add(project));
796        }
797        
798        Set<String> projectsPopulations = projects.stream()
799                                                  .map(project -> project.getSites())
800                                                  .flatMap(Collection::stream)
801                                                  .map(site -> _populationContextHelper.getUserPopulationsOnContext("/sites/" + site.getName(), false))
802                                                  .flatMap(Set::stream)
803                                                  .collect(Collectors.toSet());
804        
805        Set<JCRProjectMember> members = projects.stream()
806                                                .map(project -> getProjectMembers(project))
807                                                .flatMap(Set::stream)
808                                                .collect(Collectors.toSet());
809        
810        Set<UserIdentity> users = members.stream()
811                                         .filter(member -> MemberType.USER.toString().equals(member.getType()))
812                                         .map(JCRProjectMember::getUser)
813                                         .collect(Collectors.toSet());
814 
815        users.addAll(members.stream()
816                            .filter(member -> MemberType.GROUP.toString().equals(member.getType()))
817                            .map(member -> _groupManager.getGroup(member.getGroup()))
818                            .filter(Objects::nonNull)
819                            .map(group -> group.getUsers())
820                            .flatMap(Set::stream)
821                            .filter(user -> projectsPopulations.contains(user.getPopulationId()))
822                            .collect(Collectors.toSet()));
823        
824        result.put("users", users.stream()
825                                 .map(identity -> _userHelper.getUser(identity))
826                                 .filter(Objects::nonNull)
827                                 .map(user -> _userHelper.user2json(user, true))
828                                 .collect(Collectors.toList()));
829        
830        return result;
831    }
832    
833    /**
834     * Get the list of users of the project
835     * @param projectName The project name
836     * @return The list of users
837     * @throws AmetysRepositoryException If an error occurred
838     * @throws IllegalAccessException If an error occurred
839     */
840    @Callable
841    public Map<String, Object> getProjectMembers(String projectName) throws IllegalAccessException, AmetysRepositoryException
842    {
843        Project project = _projectManager.getProject(projectName);
844        Set<JCRProjectMember> members = getProjectMembers(project);
845        List<Map<String, Object>> membersList = new ArrayList<>();
846        
847        if (!_projectRightHelper.hasReadAccess(project))
848        {
849            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without reader right in the project " + project.getPath());
850        }
851            
852        for (JCRProjectMember projectMember : members)
853        {
854            Map<String, Object> memberData = new HashMap<>();
855            boolean memberExists = false;
856            if (MemberType.USER.toString().equals(projectMember.getType()))
857            {
858                UserIdentity userIdentity = projectMember.getUser();
859                User user = _userManager.getUser(userIdentity);
860                if (user != null)
861                {
862                    memberData.putAll(_userHelper.user2json(user));
863                    memberData.put("type", MemberType.USER.toString());
864                    memberData.put("id", UserIdentity.userIdentityToString(userIdentity));
865                    memberExists = true;
866                }
867            }
868            else if (MemberType.GROUP.toString().equals(projectMember.getType()))
869            {
870                GroupIdentity groupIdentity = projectMember.getGroup();
871                Group group = _groupManager.getGroup(groupIdentity);
872                if (group != null)
873                {
874                    memberData.putAll(group2Json(group));
875                    memberData.put("type", MemberType.GROUP.toString());
876                    memberExists = true;
877                }
878            }
879                
880            if (memberExists)
881            {
882                String role = projectMember.getRole();
883                if (role != null)
884                {
885                    memberData.put("role", role);
886                }
887                
888                Map<String, Object> rights = new HashMap<>();
889                
890                rights.put("edit", _projectRightHelper.hasRight(__RIGHTS_ADD_MEMBER_TO_PROJECT, project));
891                rights.put("delete", _projectRightHelper.hasRight(__RIGHTS_REMOVE_MEMBER_TO_PROJECT, project));
892                memberData.put("rights", rights);
893                
894                membersList.add(memberData);
895            }
896        }
897
898        Map<String, Object> result = new HashMap<>();
899        result.put("members", membersList);
900        
901        return result;
902    }
903    
904    /**
905     * Retrieves the rights for the current user in the project
906     * @param projectName The project Name
907     * @return The project
908     */
909    @Callable
910    public Map<String, Object> getMemberModuleRights(String projectName)
911    {
912        Map<String, Object> rights = new HashMap<>();
913        
914        Project project = _projectManager.getProject(projectName);
915        if (project == null)
916        {
917            rights.put("message", "unknow-project");
918            return rights;
919        }
920        rights.put("view", _projectRightHelper.hasReadAccess(project));
921        rights.put("add", _projectRightHelper.hasRight(__RIGHTS_ADD_MEMBER_TO_PROJECT, project));
922        
923        return rights;
924    }
925    
926    /**
927     * Get the list of users of the project
928     * @param project The project
929     * @return The list of users
930     */
931    public Set<JCRProjectMember> getProjectMembers(Project project)
932    {
933        Set<JCRProjectMember> projectUsers = new HashSet<>();
934        
935        if (project != null)
936        {
937            ModifiableTraversableAmetysObject usersNode = _getProjectMembersNode(project);
938            
939            for (AmetysObject userNode : usersNode.getChildren())
940            {
941                if (userNode instanceof JCRProjectMember)
942                {
943                    projectUsers.add((JCRProjectMember) userNode);
944                }
945            }
946        }
947        
948        return projectUsers;
949    }
950    
951    /**
952     * Test if an user is a member of a project
953     * @param project The project
954     * @param userIdentity The user identity
955     * @return True if this user is a member of this project
956     */
957    public boolean isProjectMember(Project project, UserIdentity userIdentity)
958    {
959        if (userIdentity == null)
960        {
961            return false;
962        }
963        
964        Set<JCRProjectMember> members = getProjectMembers(project);
965        
966        boolean isMember = members.stream()
967                                  .filter(member -> MemberType.USER.toString().equals(member.getType()))
968                                  .anyMatch(member -> userIdentity.equals(member.getUser()));
969
970        if (!isMember)
971        {
972            Set<String> projectPopulations = project.getSites()
973                                                    .stream()
974                                                    .map(site -> _populationContextHelper.getUserPopulationsOnContexts(ImmutableList.of("/sites/" + site.getName(), "/sites-fo/" + site.getName()), false, false))
975                                                    .flatMap(Set::stream)
976                                                    .collect(Collectors.toSet());
977            
978            isMember = members.stream()
979                              .filter(member -> MemberType.GROUP.toString().equals(member.getType()))
980                              .map(member -> _groupManager.getGroup(member.getGroup()))
981                              .filter(Objects::nonNull)
982                              .map(group -> group.getUsers())
983                              .flatMap(Set::stream)
984                              .filter(user -> projectPopulations.contains(user.getPopulationId()))
985                              .anyMatch(member -> userIdentity.equals(member));
986        }
987        return isMember;
988    }
989    
990    /**
991     * Set the manager of a project
992     * @param projectName The project name
993     * @param profileId The profile id to affect
994     * @param userIdentity The user
995     */
996    public void setProjectManager(String projectName, String profileId, UserIdentity userIdentity)
997    {
998        Project project = _projectManager.getProject(projectName);
999        if (project != null)
1000        {
1001            UserIdentity previousManager = project.getManager();
1002            if (previousManager != null && previousManager != userIdentity)
1003            {
1004                _removeManagerRights(project, previousManager);
1005            }
1006            else
1007            {
1008                // Remove obsolete rights if the profile changed but the manager stayed the same
1009                _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(project, userIdentity).stream()
1010                        .filter(previousProfile -> !profileId.equals(previousProfile) && !RightManager.READER_PROFILE_ID.equals(previousProfile))
1011                        .forEach(obsoleteProfile -> _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(userIdentity, obsoleteProfile, project));
1012            }
1013            
1014            JCRProjectMember member = getOrCreateProjectMember(project, userIdentity);
1015            
1016            project.setManager(userIdentity);
1017            
1018            _profileAssignmentStorageExtensionPoint.allowProfileToUser(userIdentity, RightManager.READER_PROFILE_ID, project);
1019            _profileAssignmentStorageExtensionPoint.allowProfileToUser(userIdentity, profileId, project);
1020            _notifyAclUpdated(userIdentity, project, Arrays.asList(RightManager.READER_PROFILE_ID, profileId));
1021            
1022            // Allow the user to access the project's site
1023            _allowReadAccessOnProjectDashboard(member, project);
1024            
1025            for (WorkspaceModule module : _projectManager.getModules(project))
1026            {
1027                ModifiableResourceCollection moduleRootNode = module.getModuleRoot(project, false);
1028                _profileAssignmentStorageExtensionPoint.allowProfileToUser(userIdentity, RightManager.READER_PROFILE_ID, moduleRootNode);
1029                _profileAssignmentStorageExtensionPoint.allowProfileToUser(userIdentity, profileId, moduleRootNode);
1030                _notifyAclUpdated(userIdentity, moduleRootNode, Arrays.asList(RightManager.READER_PROFILE_ID, profileId));
1031                
1032                // Update read access on project module's pages
1033                AmetysObjectIterable<Page> modulePages = module.getModulePages(project, null);
1034                for (Page modulePage : modulePages)
1035                {
1036                    _updateReadAccessOnModulePage(member, modulePage, true);
1037                }
1038            }
1039            
1040            project.saveChanges();
1041        }
1042    }
1043    
1044    private void _removeManagerRights(Project project, UserIdentity userIdentity)
1045    {
1046        _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(project, userIdentity).stream()
1047                .filter(((Predicate<String>) RightManager.READER_PROFILE_ID::equals).negate()) // keep reader profile for project access, only remove manager specific rights
1048                .forEach(profile -> _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(userIdentity, profile, project));
1049    }
1050    
1051    private void _notifyAclUpdated(UserIdentity userIdentity, AmetysObject aclContext, Collection<String> aclProfiles)
1052    {
1053        Map<String, Object> eventParams = new HashMap<>();
1054        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, aclContext);
1055        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, aclContext.getId());
1056        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, aclProfiles);
1057        
1058        _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, userIdentity, eventParams));
1059    }
1060
1061    /**
1062     * Retrieve or create a user in a project
1063     * @param project The project
1064     * @param userIdentity the user
1065     * @return The user
1066     */
1067    public JCRProjectMember getOrCreateProjectMember(Project project, UserIdentity userIdentity)
1068    {
1069        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.USER.toString().equals(((JCRProjectMember) memberNode).getType()) 
1070                && userIdentity.equals(((JCRProjectMember) memberNode).getUser());
1071        JCRProjectMember projectMember = _getOrCreateProjectMember(project, findMemberPredicate);
1072        
1073        if (projectMember.needsSave())
1074        {
1075            projectMember.setUser(userIdentity);
1076            projectMember.setType(MemberType.USER);
1077        }
1078        
1079        return projectMember; 
1080    }
1081
1082    /**
1083     * Retrieve or create a group as a member in a project
1084     * @param project The project
1085     * @param groupIdentity the group
1086     * @return The user
1087     */
1088    public JCRProjectMember getOrCreateProjectMember(Project project, GroupIdentity groupIdentity)
1089    {
1090        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.GROUP.toString().equals(((JCRProjectMember) memberNode).getType()) 
1091                && groupIdentity.equals(((JCRProjectMember) memberNode).getGroup());
1092        JCRProjectMember projectMember = _getOrCreateProjectMember(project, findMemberPredicate);
1093        
1094        if (projectMember.needsSave())
1095        {
1096            projectMember.setGroup(groupIdentity);
1097            projectMember.setType(MemberType.GROUP);
1098        }
1099        
1100        return projectMember; 
1101    }
1102    
1103    /**
1104     * Retrieve or create a member in a project
1105     * @param project The project
1106     * @param findMemberPredicate The predicate to find the member node
1107     * @return The member node. A new node is created if the member node was not found
1108     */
1109    protected JCRProjectMember _getOrCreateProjectMember(Project project, Predicate<? super AmetysObject> findMemberPredicate)
1110    {
1111        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1112        
1113        Optional<AmetysObject> member = _getProjectMembersNode(project).getChildren()
1114                                                                       .stream()
1115                                                                       .filter(memberNode -> memberNode instanceof JCRProjectMember)
1116                                                                       .filter(findMemberPredicate)
1117                                                                       .findFirst();
1118        if (member.isPresent())
1119        {
1120            return (JCRProjectMember) member.get();
1121        }
1122        
1123        String baseName = "member";
1124        String name = baseName;
1125        int index = 1;
1126        while (membersNode.hasChild(name))
1127        {
1128            index++;
1129            name = baseName + "-" + index;
1130        }
1131        
1132        return membersNode.createChild(name, __PROJECT_MEMBER_NODE_TYPE);
1133    }
1134
1135    
1136    /**
1137     * Remove a user from a project
1138     * @param projectName The project name
1139     * @param identity The identity of the user or group, who must be a member of the project 
1140     * @param type The type of the member, user or group
1141     * @return The error code, if an error occurred
1142     * @throws IllegalAccessException If the user cannot execute this operation
1143     */
1144    @Callable
1145    public Map<String, Object> removeMember(String projectName, String identity, String type) throws IllegalAccessException
1146    {
1147        Map<String, Object> result = new HashMap<>();
1148        
1149        boolean isTypeUser = JCRProjectMember.MemberType.USER.toString().equals(type);
1150        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.toString().equals(type);
1151        UserIdentity user = Optional.ofNullable(identity)
1152                                    .filter(id -> id != null && isTypeUser)
1153                                    .map(UserIdentity::stringToUserIdentity)
1154                                    .orElse(null);
1155        GroupIdentity group = Optional.ofNullable(identity)
1156                                      .filter(id -> id != null && isTypeGroup)
1157                                      .map(GroupIdentity::stringToGroupIdentity)
1158                                      .orElse(null);
1159        
1160        if ((isTypeGroup && group == null) || (isTypeUser && user == null))
1161        {
1162            result.put("message", isTypeGroup ? "unknow-group" : "unknow-user");
1163            return result;
1164        }
1165        
1166        Project project = _projectManager.getProject(projectName);
1167        if (project == null)
1168        {
1169            result.put("message", "unknow-project");
1170            return result;
1171        }
1172        
1173        if (!_projectRightHelper.hasRight(__RIGHTS_REMOVE_MEMBER_TO_PROJECT, project))
1174        {
1175            throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without convenient right [" + __RIGHTS_REMOVE_MEMBER_TO_PROJECT + ", /projects" + project.getPath() + "]");
1176        }
1177        
1178        JCRProjectMember projectMember = null;
1179        if (isTypeUser)
1180        {
1181            projectMember = _getProjectMember(project, user);
1182        }
1183        else if (isTypeGroup)
1184        {
1185            projectMember = _getProjectMember(project, group);
1186        }
1187        
1188        if (projectMember == null)
1189        {
1190            result.put("message", "unknow-member");
1191            return result;
1192        }
1193        
1194        _removeMemberProfiles(project, projectMember);
1195
1196        projectMember.remove();
1197        project.saveChanges();
1198        
1199        Map<String, Object> eventParams = new HashMap<>();
1200        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, identity);
1201        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, type);
1202        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1203        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_DELETED, _currentUserProvider.getUser(), eventParams));
1204        
1205        return result;
1206    }
1207
1208    private JCRProjectMember _getProjectMember(Project project, GroupIdentity group)
1209    {
1210        JCRProjectMember projectMember = null;
1211        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1212        
1213        for (AmetysObject memberNode : membersNode.getChildren())
1214        {
1215            if (memberNode instanceof JCRProjectMember)
1216            {
1217                JCRProjectMember member = (JCRProjectMember) memberNode;
1218                if (MemberType.GROUP.toString().equals(member.getType()) && group.equals(member.getGroup()))
1219                {
1220                    projectMember = (JCRProjectMember) memberNode;
1221                }
1222
1223            }
1224        }
1225        return projectMember;
1226    }
1227
1228    private JCRProjectMember _getProjectMember(Project project, UserIdentity user)
1229    {
1230        JCRProjectMember projectMember = null;
1231        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1232        
1233        for (AmetysObject memberNode : membersNode.getChildren())
1234        {
1235            if (memberNode instanceof JCRProjectMember)
1236            {
1237                JCRProjectMember member = (JCRProjectMember) memberNode;
1238                if (MemberType.USER.toString().equals(member.getType()) && user.equals(member.getUser()))
1239                {
1240                    projectMember = (JCRProjectMember) memberNode;
1241                }
1242            }
1243        }
1244        return projectMember;
1245    }
1246
1247    private void _removeMemberProfiles(Project project, JCRProjectMember projectMember)
1248    {
1249        // Remove rights on project and project's site
1250        _removeReadAccessOnProjectDashboard(projectMember, project);
1251        
1252        for (WorkspaceModule module : _projectManager.getModules(project))
1253        {
1254            ModifiableResourceCollection moduleRootNode = module.getModuleRoot(project, false);
1255            // Remove profiles on module
1256            _removeMemberProfiles(projectMember, moduleRootNode);
1257            
1258            // Remove profiles on module's pages
1259            AmetysObjectIterable<Page> modulePages = module.getModulePages(project, null);
1260            for (Page modulePage : modulePages)
1261            {
1262                _removeMemberProfiles(projectMember, modulePage);
1263            }
1264        }
1265        
1266        // Remove rights on project
1267        _removeMemberProfiles(projectMember, project);
1268    }
1269    
1270    /**
1271     * Retrieves the users node of the project
1272     * The users node will be created if necessary
1273     * @param project The project
1274     * @return The users node of the project
1275     */
1276    protected ModifiableTraversableAmetysObject _getProjectMembersNode(Project project)
1277    {
1278        if (project == null)
1279        {
1280            throw new AmetysRepositoryException("Error getting the project users node, project is null");
1281        }
1282        
1283        try
1284        {
1285            ModifiableTraversableAmetysObject usersNode;
1286            if (project.hasChild(__PROJECT_MEMBERS_NODE))
1287            {
1288                usersNode = project.getChild(__PROJECT_MEMBERS_NODE);
1289            }
1290            else
1291            {
1292                usersNode = project.createChild(__PROJECT_MEMBERS_NODE, __PROJECT_MEMBERS_NODE_TYPE);
1293            }
1294            
1295            return usersNode;
1296        }
1297        catch (AmetysRepositoryException e)
1298        {
1299            throw new AmetysRepositoryException("Error getting the project users node", e);
1300        }
1301    }
1302    
1303    /**
1304     * Get the JSON representation of a group
1305     * @param group The group
1306     * @return The group
1307     */
1308    protected Map<String, Object> group2Json(Group group)
1309    {
1310        Map<String, Object> infos = new HashMap<>();
1311        infos.put("id", group.getIdentity().getId());
1312        infos.put("label", group.getLabel());
1313        infos.put("sortablename", group.getLabel());
1314        infos.put("groupDirectory", group.getIdentity().getDirectoryId());
1315        return infos;
1316    }
1317}