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.Comparator;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Objects;
027import java.util.Optional;
028import java.util.Set;
029import java.util.TreeSet;
030import java.util.function.BiPredicate;
031import java.util.function.Predicate;
032import java.util.regex.Matcher;
033import java.util.regex.Pattern;
034import java.util.stream.Collectors;
035
036import org.apache.avalon.framework.activity.Disposable;
037import org.apache.avalon.framework.activity.Initializable;
038import org.apache.avalon.framework.component.Component;
039import org.apache.avalon.framework.context.Context;
040import org.apache.avalon.framework.context.ContextException;
041import org.apache.avalon.framework.context.Contextualizable;
042import org.apache.avalon.framework.service.ServiceException;
043import org.apache.avalon.framework.service.ServiceManager;
044import org.apache.avalon.framework.service.Serviceable;
045import org.apache.cocoon.components.ContextHelper;
046import org.apache.cocoon.environment.Request;
047import org.apache.commons.collections.CollectionUtils;
048import org.apache.commons.lang3.StringUtils;
049import org.apache.http.annotation.Obsolete;
050
051import org.ametys.cms.languages.Language;
052import org.ametys.cms.languages.LanguagesManager;
053import org.ametys.cms.repository.Content;
054import org.ametys.cms.transformation.URIResolverExtensionPoint;
055import org.ametys.core.cache.AbstractCacheManager;
056import org.ametys.core.cache.Cache;
057import org.ametys.core.group.Group;
058import org.ametys.core.group.GroupDirectoryContextHelper;
059import org.ametys.core.group.GroupIdentity;
060import org.ametys.core.group.GroupManager;
061import org.ametys.core.observation.AsyncObserver;
062import org.ametys.core.observation.Event;
063import org.ametys.core.observation.ObservationManager;
064import org.ametys.core.right.ProfileAssignmentStorage.UserOrGroup;
065import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint;
066import org.ametys.core.right.RightManager;
067import org.ametys.core.right.RightProfilesDAO;
068import org.ametys.core.ui.Callable;
069import org.ametys.core.user.CurrentUserProvider;
070import org.ametys.core.user.User;
071import org.ametys.core.user.UserIdentity;
072import org.ametys.core.user.UserManager;
073import org.ametys.core.user.directory.NotUniqueUserException;
074import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
075import org.ametys.plugins.core.user.UserHelper;
076import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
077import org.ametys.plugins.repository.AmetysObject;
078import org.ametys.plugins.repository.AmetysObjectIterable;
079import org.ametys.plugins.repository.AmetysObjectResolver;
080import org.ametys.plugins.repository.AmetysRepositoryException;
081import org.ametys.plugins.repository.ModifiableAmetysObject;
082import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
083import org.ametys.plugins.repository.RepositoryConstants;
084import org.ametys.plugins.repository.query.expression.Expression.Operator;
085import org.ametys.plugins.repository.query.expression.UserExpression;
086import org.ametys.plugins.userdirectory.UserDirectoryHelper;
087import org.ametys.plugins.userdirectory.page.UserDirectoryPageResolver;
088import org.ametys.plugins.userdirectory.page.UserPage;
089import org.ametys.plugins.workspaces.ObservationConstants;
090import org.ametys.plugins.workspaces.WorkspacesHelper;
091import org.ametys.plugins.workspaces.documents.DocumentWorkspaceModule;
092import org.ametys.plugins.workspaces.forum.ForumWorkspaceModule;
093import org.ametys.plugins.workspaces.members.JCRProjectMember.MemberType;
094import org.ametys.plugins.workspaces.project.ProjectManager;
095import org.ametys.plugins.workspaces.project.ProjectsCatalogueManager;
096import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
097import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
098import org.ametys.plugins.workspaces.project.objects.Project;
099import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus;
100import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper;
101import org.ametys.plugins.workspaces.tasks.TasksWorkspaceModule;
102import org.ametys.runtime.authentication.AccessDeniedException;
103import org.ametys.runtime.config.Config;
104import org.ametys.runtime.i18n.I18nizableText;
105import org.ametys.runtime.plugin.component.AbstractLogEnabled;
106import org.ametys.web.WebConstants;
107import org.ametys.web.WebHelper;
108import org.ametys.web.population.PopulationContextHelper;
109import org.ametys.web.repository.site.Site;
110import org.ametys.web.usermanagement.UserManagementException;
111import org.ametys.web.usermanagement.UserSignupManager;
112
113/**
114 * Helper component for managing project's users
115 */
116public class ProjectMemberManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, Initializable, AsyncObserver, Disposable
117{
118    /** Avalon Role */
119    public static final String ROLE = ProjectMemberManager.class.getName();
120    
121    /** The id of the members service */
122    public static final String __WORKSPACES_SERVICE_MEMBERS = "org.ametys.plugins.workspaces.module.Members";
123    
124    private static final String __PROJECT_MEMBER_CACHE = "projectMemberCache";
125    
126    @Obsolete // For v1 project only
127    private static final String __PROJECT_RIGHT_PROFILE = "PROJECT";
128    
129    /** Constants for users project node */
130    private static final String __PROJECT_MEMBERS_NODE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":members";
131    
132    /** The type of the project users node type */
133    private static final String __PROJECT_MEMBERS_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured";
134
135    /** The type of a project user node type */
136    private static final String __PROJECT_MEMBER_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project-member";
137
138    private static Pattern __MAIL_BETWEEN_BRACKETS_PATTERN = Pattern.compile("^[^<]*<(.*@.*)>$");
139    
140    /** Avalon context */
141    protected Context _context;
142    
143    /** Project manager */
144    protected ProjectManager _projectManager;
145
146    /** Project rights helper */
147    protected ProjectRightHelper _projectRightHelper;
148
149    /** Profiles right manager */
150    protected RightProfilesDAO _rightProfilesDAO;
151
152    /** Profile assignment storage */
153    protected ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageExtensionPoint;
154    
155    /** Ametys object resolver */
156    protected AmetysObjectResolver _resolver;
157
158    /** Rights manager */
159    protected RightManager _rightManager;
160
161    /** Current user provider */
162    protected CurrentUserProvider _currentUserProvider;
163
164    /** Users manager */
165    protected UserManager _userManager;
166
167    /** The observation manager */
168    protected ObservationManager _observationManager;
169
170    /** Module managers EP */
171    protected WorkspaceModuleExtensionPoint _moduleManagerEP;
172    
173    /** The user helper */
174    protected UserHelper _userHelper;
175
176    /** The groups manager */
177    protected GroupManager _groupManager;
178
179    /** The population context helper */
180    protected PopulationContextHelper _populationContextHelper;
181
182    /** The user directory helper */
183    protected UserDirectoryHelper _userDirectoryHelper;
184    
185    /** The project invitation helper */
186    protected ProjectInvitationHelper _projectInvitationHelper;
187
188    /** The language manager */
189    protected LanguagesManager _languagesManager;
190        
191    /** The resolver for user directory pages */
192    protected UserDirectoryPageResolver _userDirectoryPageResolver;
193
194    /** The page URI resolver. */
195    protected URIResolverExtensionPoint _uriResolver;
196
197    /** The group directory context helper */
198    protected GroupDirectoryContextHelper _groupDirectoryContextHelper;
199
200    /** The cache manager */
201    protected AbstractCacheManager _abstractCacheManager;
202    
203    /** The user signup manager */
204    protected UserSignupManager _userSignupManager;
205
206    /** The project catalogue manager component */
207    protected ProjectsCatalogueManager _projectsCatalogueManager;
208
209    /** The helper for project rights */
210    protected ProjectRightHelper _projectRightsHelper;
211
212    /** Workspace helper */
213    protected WorkspacesHelper _workspaceHelper;
214    
215    @Override
216    public void contextualize(Context context) throws ContextException
217    {
218        _context = context;
219    }
220    
221    @Override
222    public void service(ServiceManager manager) throws ServiceException
223    {
224        _abstractCacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
225        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
226        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
227        _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE);
228        _rightProfilesDAO = (RightProfilesDAO) manager.lookup(RightProfilesDAO.ROLE);
229        _profileAssignmentStorageExtensionPoint = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE);
230        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
231        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
232        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
233        _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE);
234        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
235        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
236        _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
237        _populationContextHelper = (PopulationContextHelper) manager.lookup(org.ametys.core.user.population.PopulationContextHelper.ROLE);
238        _userDirectoryHelper = (UserDirectoryHelper) manager.lookup(UserDirectoryHelper.ROLE);
239        _projectInvitationHelper = (ProjectInvitationHelper) manager.lookup(ProjectInvitationHelper.ROLE);
240        _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE);
241        _userDirectoryPageResolver = (UserDirectoryPageResolver) manager.lookup(UserDirectoryPageResolver.ROLE);
242        _uriResolver = (URIResolverExtensionPoint) manager.lookup(URIResolverExtensionPoint.ROLE);
243        _groupDirectoryContextHelper = (GroupDirectoryContextHelper) manager.lookup(GroupDirectoryContextHelper.ROLE);
244        _userSignupManager = (UserSignupManager) manager.lookup(UserSignupManager.ROLE);
245        _projectsCatalogueManager = (ProjectsCatalogueManager) manager.lookup(ProjectsCatalogueManager.ROLE);
246        _projectRightsHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE);
247        _workspaceHelper = (WorkspacesHelper) manager.lookup(WorkspacesHelper.ROLE);
248    }
249    
250    public void initialize() throws Exception
251    {
252        _abstractCacheManager.createMemoryCache(__PROJECT_MEMBER_CACHE,
253                new I18nizableText("plugin.workspaces", "PLUGIN_WORKSPACES_CACHE_PROJECT_MEMBER_LABEL"),
254                new I18nizableText("plugin.workspaces", "PLUGIN_WORKSPACES_CACHE_PROJECT_MEMBER_DESCRIPTION"),
255                true,
256                null);
257        
258        _observationManager.registerObserver(this);
259    }
260    
261    public void dispose()
262    {
263        _observationManager.unregisterObserver(this);
264    }
265    
266    /**
267     * Retrieve the data of a member of a project, or the default data if no user is provided
268     * @param projectName The name of the project
269     * @param identity The user or group identity. If null, return the default profiles for a new user
270     * @param type The type of the identity. Can be "user" or "group"
271     * @return The map of profiles per module for the user
272     */
273    @Callable
274    public Map<String, Object> getProjectMemberData(String projectName, String identity, String type)
275    {
276        Map<String, Object> result = new HashMap<>();
277        
278        boolean isTypeUser = JCRProjectMember.MemberType.USER.name().equals(type.toUpperCase());
279        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.name().equals(type.toUpperCase());
280        UserIdentity user = Optional.ofNullable(identity)
281                                    .filter(id -> id != null && isTypeUser)
282                                    .map(UserIdentity::stringToUserIdentity)
283                                    .orElse(null);
284        GroupIdentity group = Optional.ofNullable(identity)
285                                      .filter(id -> id != null && isTypeGroup)
286                                      .map(GroupIdentity::stringToGroupIdentity)
287                                      .orElse(null);
288        
289        if (identity != null)
290        {
291            if (isTypeGroup && group == null)
292            {
293                result.put("message", "unknown-group");
294                result.put("success", false);
295                return result;
296            }
297            else if (isTypeUser && user == null)
298            {
299                result.put("message", "unknown-user");
300                result.put("success", false);
301                return result;
302            }
303        }
304        
305        Project project = _projectManager.getProject(projectName);
306        if (project == null)
307        {
308            result.put("message", "unknown-project");
309            result.put("success", false);
310            return result;
311        }
312        
313        if (!_projectRightHelper.canAddMember(project))
314        {
315            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to get member's rights without convenient right [" + projectName + ", " + identity + "]");
316        }
317        
318        boolean newMember = true;
319        Map<String, String> userProfiles;
320        
321        if (user != null || group != null)
322        {
323            JCRProjectMember projectMember = user != null ? _getOrCreateJCRProjectMember(project, user) : _getOrCreateJCRProjectMember(project, group);
324            
325            newMember = projectMember.needsSave();
326            
327            String role = projectMember.getRole();
328            if (role != null)
329            {
330                result.put("role", role);
331            }
332            
333            userProfiles = _getMemberProfiles(projectMember, project);
334        }
335        else
336        {
337            userProfiles = new HashMap<>();
338        }
339        
340        result.put("profiles", userProfiles);
341        result.put("status", newMember ? "new" : "edit");
342        result.put("success", true);
343        
344        return result;
345    }
346
347    
348    /**
349     * Get right profile of a member
350     * @param member The member
351     * @param project The project name
352     * @return a map of the right profile
353     */
354    private Map<String, String> _getMemberProfiles(JCRProjectMember member, Project project)
355    {
356        Map<String, String> userProfiles = new HashMap<>();
357        
358        // Get allowed profile on modules (among the project members's profiles)
359        for (WorkspaceModule module : _projectManager.getModules(project))
360        {
361            String allowedProfileOnProject = _getAllowedProfileOnModule(project, module, member);
362            userProfiles.put(module.getId(), allowedProfileOnProject);
363        }
364        
365        return userProfiles;
366    }
367    
368    private String _getAllowedProfileOnModule (Project project, WorkspaceModule module, JCRProjectMember member)
369    {
370        Set<String> profileIds = _projectRightHelper.getProfilesIds();
371        
372        AmetysObject moduleObject = module.getModuleRoot(project, false);
373        Set<String> allowedProfilesForMember = _getAllowedProfile(member, moduleObject);
374        
375        for (String allowedProfile : allowedProfilesForMember)
376        {
377            if (profileIds.contains(allowedProfile))
378            {
379                // Get the first allowed profile among the project's members profiles
380                return allowedProfile;
381            }
382        }
383        
384        return null;
385    }
386    
387    /**
388     * Add new members and invitation by email
389     * @param projectName The project name
390     * @param newMembers The members to add (users or groups)
391     * @param invitEmails The invitation emails
392     * @return the result with errors
393     */
394    @SuppressWarnings("unchecked")
395    @Callable
396    public Map<String, Object> addMembers(String projectName, List<Map<String, String>> newMembers, List<String> invitEmails)
397    {
398        Map<String, Object> result = new HashMap<>();
399        
400        Request request = ContextHelper.getRequest(_context);
401        String siteName = WebHelper.getSiteName(request);
402        
403        boolean hasError = false;
404        boolean inviteError = false;
405        boolean unknownProject = false;
406        List<String> unknownGroups = new ArrayList<>();
407        List<String> unknownUsers = new ArrayList<>();
408        List<Map<String, Object>> existingUsers = new ArrayList<>();
409        List<Map<String, Object>> membersAdded = new ArrayList<>();
410        
411        List<String> filteredInvitEmails = new ArrayList<>();
412        if (invitEmails != null)
413        {
414            try
415            {
416                for (String invitEmail : invitEmails)
417                {
418                    
419                    
420                    String filteredInvitEmail = invitEmail;
421
422                    // Regexp pattern to extract "email@domain.com" from "FirstName LastName <email@domain.com>"
423                    // ^[^<]*<(.*@.*)>$
424                    // ^  => asserts position at start of a line
425                    // [^<]* => match any characters that are not '<', so the matched group start at the first bracket
426                    // <(.*@.*)> => match text between brackets, containing '@'
427                    Matcher matcher =  __MAIL_BETWEEN_BRACKETS_PATTERN.matcher(invitEmail);
428                    if (matcher.matches() && matcher.groupCount() == 1)
429                    {
430                        filteredInvitEmail = matcher.group(1);
431                    }
432                    
433                    Optional<User> userIfExists = _userSignupManager.getUserIfHeExists(filteredInvitEmail, siteName);
434                    if (userIfExists.isPresent())
435                    {
436                        newMembers.add(Map.of(
437                            "id", UserIdentity.userIdentityToString(userIfExists.get().getIdentity()),
438                            "type", "user"
439                        ));
440                    }
441                    else
442                    {
443                        filteredInvitEmails.add(filteredInvitEmail);
444                    }
445                }
446            }
447            catch (UserManagementException e)
448            {
449                hasError = true;
450                inviteError = true;
451                getLogger().error("Impossible to send email invitations", e);
452            }
453            catch (NotUniqueUserException e)
454            {
455                hasError = true;
456                inviteError = true;
457                getLogger().error("Impossible to send email invitations, some user already exist", e);
458            }
459        }
460        
461        for (Map<String, String> newMember : newMembers)
462        {
463            Map<String, Object> addResult = addMember(projectName, newMember.get("id"), newMember.get("type"));
464            boolean success = (boolean) addResult.get("success");
465            if (!success)
466            {
467                String error = (String) addResult.get("message");
468                if ("unknown-user".equals(error))
469                {
470                    hasError = true;
471                    unknownUsers.add(newMember.get("id"));
472                }
473                else if ("unknown-group".equals(error))
474                {
475                    hasError = true;
476                    unknownGroups.add(newMember.get("id"));
477                }
478                else if ("unknown-project".equals(error))
479                {
480                    hasError = true;
481                    unknownProject = true;
482                }
483                else if ("existing-user".equals(error))
484                {
485                    existingUsers.add((Map<String, Object>) addResult.get("existing-user"));
486                }
487            }
488            else
489            {
490                membersAdded.add((Map<String, Object>) addResult.get("member"));
491            }
492        }
493        
494        if (!filteredInvitEmails.isEmpty())
495        {
496            Map<String, String> newProfiles = _getDefaultProfilesByModule();
497            
498            try
499            {
500                Map<String, Object> inviteEmails = _projectInvitationHelper.inviteEmails(projectName, filteredInvitEmails, newProfiles);
501                List<String> errors = (List<String>) inviteEmails.get("email-error");
502                if (!errors.isEmpty())
503                {
504                    hasError = true;
505                    inviteError = true;
506                }
507                existingUsers.addAll((List<Map<String, Object>>) inviteEmails.get("existing-users"));
508            }
509            catch (UserManagementException e)
510            {
511                hasError = true;
512                inviteError = true;
513                getLogger().error("Impossible to send email invitations", e);
514            }
515            catch (NotUniqueUserException e)
516            {
517                hasError = true;
518                inviteError = true;
519                getLogger().error("Impossible to send email invitations, some user already exist", e);
520            }
521            
522        }
523        
524        result.put("invite-error", inviteError);
525        result.put("existing-users", existingUsers);
526        result.put("unknown-groups", unknownGroups);
527        result.put("unknown-users", unknownUsers);
528        result.put("unknown-project", unknownProject);
529        result.put("members-added", membersAdded);
530        result.put("success", !hasError);
531        
532        return result;
533    }
534    
535    /**
536     * Add a new member
537     * @param projectName The project name
538     * @param identity The user or group identity.
539     * @param type The type of the identity. Can be "user" or "group"
540     * @return the result
541     */
542    @Callable
543    public Map<String, Object> addMember(String projectName, String identity, String type)
544    {
545        Map<String, String> newProfiles = _getDefaultProfilesByModule();
546        
547        return _setProjectMemberData(projectName, identity, type, newProfiles, null, true);
548    }
549    
550    /**
551     * Get a map of each available module, with the default profile
552     * @return A map with moduleId : profileId
553     */
554    protected Map<String, String> _getDefaultProfilesByModule()
555    {
556        Map<String, String> newProfiles = new HashMap<>();
557        
558        String defaultProfile = StringUtils.defaultString(Config.getInstance().getValue("workspaces.profile.default"));
559        for (String moduleId : _moduleManagerEP.getExtensionsIds())
560        {
561            newProfiles.put(moduleId, defaultProfile);
562        }
563        
564        return newProfiles;
565    }
566    
567    /**
568     * Set the user data in the project
569     * @param projectName The project name
570     * @param identity The user or group identity.
571     * @param type The type of the identity. Can be "user" or "group"
572     * @param newProfiles The profiles to affect, mapped by module
573     * @param role The user role inside the project
574     * @return The result
575     */
576    @Callable
577    public Map<String, Object> setProjectMemberData(String projectName, String identity, String type, Map<String, String> newProfiles, String role)
578    {
579        return _setProjectMemberData(projectName, identity, type, newProfiles, role, false);
580    }
581    
582    /**
583     * Set the user data in the project
584     * @param projectName The project name
585     * @param identity The user or group identity.
586     * @param type The type of the identity. Can be "user" or "group"
587     * @param newProfiles The profiles to affect, mapped by module
588     * @param role The user role inside the project
589     * @param isNewUser <code>true</code> if the user is just added
590     * @return The result
591     */
592    protected Map<String, Object> _setProjectMemberData(String projectName, String identity, String type, Map<String, String> newProfiles, String role, boolean isNewUser)
593    {
594        Map<String, Object> result = new HashMap<>();
595        Project project = _projectManager.getProject(projectName);
596        if (project == null)
597        {
598            result.put("success", false);
599            result.put("message", "unknown-project");
600            return result;
601        }
602        
603        boolean isTypeUser = JCRProjectMember.MemberType.USER.name().equals(type.toUpperCase());
604        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.name().equals(type.toUpperCase());
605        UserIdentity user = Optional.ofNullable(identity)
606                                    .filter(id -> id != null && isTypeUser)
607                                    .map(UserIdentity::stringToUserIdentity)
608                                    .orElse(null);
609        GroupIdentity group = Optional.ofNullable(identity)
610                                      .filter(id -> id != null && isTypeGroup)
611                                      .map(GroupIdentity::stringToGroupIdentity)
612                                      .orElse(null);
613        
614        if (group == null && user == null)
615        {
616            result.put("success", false);
617            result.put("message", isTypeGroup ? "unknown-group" : "unknown-user");
618            return result;
619        }
620        
621        if (isNewUser && !_projectRightHelper.canAddMember(project) || !isNewUser && !_projectRightHelper.canEditMember(project))
622        {
623            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to set member rights without convenient right [" + projectName + ", " + identity + "]");
624        }
625        
626        if (isNewUser && isTypeUser && _getProjectMember(project, user) != null)
627        {
628            result.put("success", false);
629            result.put("message", "existing-user");
630            result.put("existing-user", _userHelper.user2json(user, true));
631            
632            return result;
633        }
634        
635        JCRProjectMember projectMember  = isTypeUser ? addOrUpdateProjectMember(project, user, newProfiles) : addOrUpdateProjectMember(project, group, newProfiles);
636        if (projectMember != null)
637        {
638            Request request = ContextHelper.getRequest(_context);
639            String lang = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
640            
641            ProjectMember member = isTypeUser ? new ProjectMember(_userManager.getUser(user), projectMember.getRole(), false) : new ProjectMember(_groupManager.getGroup(group));
642            result.put("member", _member2Json(member, lang));
643        }
644        
645        result.put("success", projectMember != null);
646        return result;
647    }
648    
649    /**
650     * Add a user to a project with open inscriptions, using the default values
651     * @param project The project
652     * @param user The user
653     * @return the added member in case of success, null otherwise
654     */
655    public JCRProjectMember addProjectMember(Project project, UserIdentity user)
656    {
657        InscriptionStatus inscriptionStatus = project.getInscriptionStatus();
658        if (!inscriptionStatus.equals(InscriptionStatus.OPEN))
659        {
660            return null;
661        }
662        
663        return addOrUpdateProjectMember(project, user, Map.of());
664    }
665    
666    /**
667     * Add a user to a project, using the provided profile values
668     * @param project The project
669     * @param user The user
670     * @param allowedProfiles the profile values
671     * @return the added member in case of success, null otherwise
672     */
673    public JCRProjectMember addOrUpdateProjectMember(Project project, UserIdentity user, Map<String, String> allowedProfiles)
674    {
675        return addOrUpdateProjectMember(project, user, allowedProfiles, _currentUserProvider.getUser());
676    }
677    /**
678     * Add a user to a project, using the provided profile values
679     * @param project The project
680     * @param user The user
681     * @param allowedProfiles the profile values
682     * @param issuer identity of the user that approved the member
683     * @return the added member in case of success, null otherwise
684     */
685    public JCRProjectMember addOrUpdateProjectMember(Project project, UserIdentity user, Map<String, String> allowedProfiles, UserIdentity issuer)
686    {
687        if (user == null)
688        {
689            return null;
690        }
691        
692        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, user);
693        _setMemberProfiles(allowedProfiles, projectMember, project);
694        _saveAndNotifyProjectMemberUpdate(project, projectMember, UserIdentity.userIdentityToString(user), issuer);
695        return projectMember;
696    }
697    
698    /**
699     * Add a group to a project, using the provided profile values
700     * @param project The project
701     * @param group The group
702     * @param allowedProfiles the profile values
703     * @return the added member in case of success, null otherwise
704     */
705    public JCRProjectMember addOrUpdateProjectMember(Project project, GroupIdentity group, Map<String, String> allowedProfiles)
706    {
707        if (group == null)
708        {
709            return null;
710        }
711        
712        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, group);
713        _setMemberProfiles(allowedProfiles, projectMember, project);
714        _saveAndNotifyProjectMemberUpdate(project, projectMember, GroupIdentity.groupIdentityToString(group), _currentUserProvider.getUser());
715        return projectMember;
716    }
717
718    private void _saveAndNotifyProjectMemberUpdate(Project project, JCRProjectMember projectMember, String userIdentityString, UserIdentity issuer)
719    {
720        project.saveChanges();
721        
722        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
723        
724        // Notify listeners
725        Map<String, Object> eventParams = new HashMap<>();
726        eventParams.put(ObservationConstants.ARGS_MEMBER, projectMember);
727        eventParams.put(ObservationConstants.ARGS_MEMBER_ID, projectMember.getId());
728        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
729        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
730        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, userIdentityString);
731        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, projectMember.getType());
732        
733        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_ADDED, issuer, eventParams));
734    }
735
736    /**
737     * Set the profiles for a member
738     * @param newProfiles The allowed profile by module
739     * @param projectMember The member
740     * @param project The project
741     */
742    private void _setMemberProfiles(Map<String, String> newProfiles, JCRProjectMember projectMember, Project project)
743    {
744        String defaultProfile = project.getDefaultProfile();
745        Set<String> defaultProfiles;
746        if (StringUtils.isEmpty(defaultProfile))
747        {
748            defaultProfiles = Set.of();
749        }
750        else
751        {
752            defaultProfiles = Set.of(defaultProfile);
753        }
754        
755        for (WorkspaceModule module : _moduleManagerEP.getModules())
756        {
757            Set<String> moduleProfiles;
758            if (newProfiles.containsKey(module.getId()))
759            {
760                String profile = newProfiles.get(module.getId());
761                moduleProfiles = StringUtils.isEmpty(profile) ? Set.of() : Set.of(profile);
762            }
763            else
764            {
765                moduleProfiles = defaultProfiles;
766            }
767            setProfileOnModule(projectMember, project, module, moduleProfiles);
768        }
769    }
770    
771    /**
772     * Affect profiles for a member on a given module
773     * @param member The member
774     * @param project The project
775     * @param module The module
776     * @param allowedProfiles The allowed profiles for the module
777     */
778    public void setProfileOnModule(JCRProjectMember member, Project project, WorkspaceModule module, Set<String> allowedProfiles)
779    {
780        if (module != null && _projectManager.isModuleActivated(project, module.getId()))
781        {
782            AmetysObject moduleObject = module.getModuleRoot(project, false);
783            _setMemberProfiles(member, allowedProfiles, moduleObject);
784        }
785    }
786    
787    private Set<String> _getAllowedProfile(JCRProjectMember member, AmetysObject object)
788    {
789        if (MemberType.GROUP == member.getType())
790        {
791            Map<GroupIdentity, Map<UserOrGroup, Set<String>>> profilesForGroups = _profileAssignmentStorageExtensionPoint.getProfilesForGroups(object, Set.of(member.getGroup()));
792            return Optional.ofNullable(profilesForGroups.get(member.getGroup())).map(a -> a.get(UserOrGroup.ALLOWED)).orElse(Set.of());
793        }
794        else
795        {
796            Map<UserIdentity, Map<UserOrGroup, Set<String>>> profilesForUsers = _profileAssignmentStorageExtensionPoint.getProfilesForUsers(object, member.getUser());
797            return Optional.ofNullable(profilesForUsers.get(member.getUser())).map(a -> a.get(UserOrGroup.ALLOWED)).orElse(Set.of());
798        }
799    }
800
801    private void _setMemberProfiles(JCRProjectMember member, Set<String> allowedProfiles, AmetysObject object)
802    {
803        Set<String> currentAllowedProfiles = _getAllowedProfile(member, object);
804        
805        Collection<String> profilesToRemove  = CollectionUtils.removeAll(currentAllowedProfiles, allowedProfiles);
806        
807        Collection<String> profilesToAdd  = CollectionUtils.removeAll(allowedProfiles, currentAllowedProfiles);
808        
809        for (String profileId : profilesToRemove)
810        {
811            _removeProfile(member, profileId, object);
812        }
813        
814        for (String profileId : profilesToAdd)
815        {
816            _addProfile(member, profileId, object);
817        }
818        
819        Collection<String> updatedProfiles = CollectionUtils.union(profilesToAdd, profilesToRemove);
820        
821        if (updatedProfiles.size() > 0)
822        {
823            _notifyAclUpdated(_currentUserProvider.getUser(), object, updatedProfiles);
824        }
825    }
826    
827    private void _removeProfile(JCRProjectMember member, String profileId, AmetysObject aclObject)
828    {
829        if (MemberType.GROUP == member.getType())
830        {
831            _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), profileId, aclObject);
832        }
833        else
834        {
835            _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), profileId, aclObject);
836        }
837    }
838    
839    private void _addProfile(JCRProjectMember member, String profileId, AmetysObject aclObject)
840    {
841        if (MemberType.GROUP == member.getType())
842        {
843            _profileAssignmentStorageExtensionPoint.allowProfileToGroup(member.getGroup(), profileId, aclObject);
844        }
845        else
846        {
847            _profileAssignmentStorageExtensionPoint.allowProfileToUser(member.getUser(), profileId, aclObject);
848        }
849    }
850
851    private void _removeMemberProfiles(JCRProjectMember member, AmetysObject object)
852    {
853        Set<String> currentAllowedProfiles = _getAllowedProfile(member, object);
854        
855        for (String allowedProfile : currentAllowedProfiles)
856        {
857            _removeProfile(member, allowedProfile, object);
858        }
859        
860        if (currentAllowedProfiles.size() > 0)
861        {
862            ((ModifiableAmetysObject) object).saveChanges();
863            
864            Map<String, Object> eventParams = new HashMap<>();
865            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, object);
866            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, object.getId());
867            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, currentAllowedProfiles);
868            eventParams.put(org.ametys.cms.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
869            
870            _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
871        }
872    }
873    
874    /**
875     * Get the current user information
876     * @return The user
877     */
878    @Callable
879    public Map<String, Object> getCurrentUser()
880    {
881        Map<String, Object> result = new HashMap<>();
882        result.put("user", _userHelper.user2json(_currentUserProvider.getUser()));
883        return result;
884    }
885    
886    /**
887     * Get the members of current project or all the members of all projects in where is no current project
888     * @return The members
889     */
890    @Callable
891    public Map<String, Object> getProjectMembers()
892    {
893        Map<String, Object> result = new HashMap<>();
894        
895        Request request = ContextHelper.getRequest(_context);
896        String projectName = (String) request.getAttribute("projectName");
897        
898        Collection<Project> projects = new ArrayList<>();
899        
900        if (StringUtils.isNotEmpty(projectName))
901        {
902            projects.add(_projectManager.getProject(projectName));
903        }
904        else
905        {
906            _projectManager.getProjects()
907                           .stream()
908                           .forEach(project -> projects.add(project));
909        }
910                                        
911        result.put("users", projects.stream()
912                                    .map(project -> getProjectMembers(project, true))
913                                    .flatMap(Set::stream)
914                                    .map(ProjectMember::getUser)
915                                    .distinct()
916                                    .map(user -> _userHelper.user2json(user, true))
917                                    .collect(Collectors.toList()));
918        
919        return result;
920    }
921    
922    /**
923     * Get the members of a project, sorted by managers, non empty role and name
924     * @param projectName the project's name
925     * @param lang the language to get user content
926     * @return the members of project
927     * @throws IllegalAccessException if an error occurred
928     * @throws AmetysRepositoryException if an error occurred
929     */
930    @Callable
931    public Map<String, Object> getProjectMembers(String projectName, String lang) throws IllegalAccessException, AmetysRepositoryException
932    {
933        return getProjectMembers(projectName, lang, false);
934    }
935    
936    /**
937     * Get the members of a project, sorted by managers, non empty role and name
938     * @param projectName the project's name
939     * @param lang the language to get user content
940     * @param expandGroup true if groups are expanded
941     * @return the members of project
942     * @throws AmetysRepositoryException if an error occurred
943     */
944    @Callable
945    public Map<String, Object> getProjectMembers(String projectName, String lang, boolean expandGroup) throws AmetysRepositoryException
946    {
947        Map<String, Object> result = new HashMap<>();
948        
949        Project project = _projectManager.getProject(projectName);
950        if (project == null)
951        {
952            result.put("message", "unknown-project");
953            result.put("success", false);
954            return result;
955        }
956        if (!_projectRightHelper.hasReadAccess(project))
957        {
958            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without reader right in the project " + project.getPath());
959        }
960        
961        List<Map<String, Object>> membersData = new ArrayList<>();
962        
963        Set<ProjectMember> projectMembers = getProjectMembers(project, expandGroup);
964        
965        for (ProjectMember projectMember : projectMembers)
966        {
967            membersData.add(_member2Json(projectMember, lang));
968        }
969        
970        result.put("members", membersData);
971        result.put("success", true);
972        
973        return result;
974    }
975    
976    private Map<String, Object> _member2Json(ProjectMember projectMember, String lang)
977    {
978        Project project = _workspaceHelper.getProjectFromRequest();
979        Map<String, Object> memberData = new HashMap<>();
980        
981        memberData.put("type", projectMember.getType().name().toLowerCase());
982        memberData.put("title", projectMember.getTitle());
983        memberData.put("sortabletitle", projectMember.getSortableTitle());
984        memberData.put("manager", projectMember.isManager());
985        
986        String role = projectMember.getRole();
987        if (StringUtils.isNotEmpty(role))
988        {
989            memberData.put("role", role);
990        }
991        
992        User user = projectMember.getUser();
993        if (user != null)
994        {
995            memberData.put("id", UserIdentity.userIdentityToString(user.getIdentity()));
996            memberData.putAll(_userHelper.user2json(user));
997
998            Content userContent = getUserContent(lang, user);
999            
1000            if (userContent != null)
1001            {
1002                if (userContent.hasValue("function"))
1003                {
1004                    memberData.put("function", userContent.getValue("function"));
1005                }
1006
1007                if (userContent.hasValue("organisation-accronym"))
1008                {
1009                    memberData.put("organisationAcronym", userContent.getValue("organisation-accronym"));
1010                }
1011                String usersDirectorySiteName = _projectManager.getUsersDirectorySiteName();
1012                String[] contentTypes = userContent.getTypes();
1013                for (String contentType : contentTypes)
1014                {
1015                    // Try to see if a user page exists for this content type
1016                    UserPage userPage = _userDirectoryPageResolver.getUserPage(userContent, usersDirectorySiteName, lang, contentType);
1017                    if (userPage != null)
1018                    {
1019                        memberData.put("link", _uriResolver.getResolverForType("page").resolve(userPage.getId(), false, true, false));
1020                    }
1021                }
1022                    
1023            }
1024            else if (getLogger().isDebugEnabled())
1025            {
1026                getLogger().debug("User content not found for user : " + user);
1027            }
1028
1029            memberData.put("hasReadAccessOnTaskModule", _projectRightsHelper.hasReadAccessOnModule(project, TasksWorkspaceModule.TASK_MODULE_ID, user.getIdentity()));
1030            memberData.put("hasReadAccessOnDocumentModule", _projectRightsHelper.hasReadAccessOnModule(project, DocumentWorkspaceModule.DOCUMENT_MODULE_ID, user.getIdentity()));
1031            memberData.put("hasReadAccessOnForumModule", _projectRightsHelper.hasReadAccessOnModule(project, ForumWorkspaceModule.FORUM_MODULE_ID, user.getIdentity()));
1032            
1033        }
1034        
1035        Group group = projectMember.getGroup();
1036        if (group != null)
1037        {
1038            memberData.putAll(group2Json(group));
1039        }
1040        
1041        return memberData;
1042    }
1043
1044    /**
1045     * Get user content
1046     * @param lang the lang
1047     * @param user the user
1048     * @return the user content or null if no exist
1049     */
1050    public Content getUserContent(String lang, User user)
1051    {
1052        Content userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), lang);
1053
1054        if (userContent == null)
1055        {
1056            userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), "en");
1057        }
1058        
1059        if (userContent == null)
1060        {
1061            Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages();
1062            for (Language availableLanguage : availableLanguages.values())
1063            {
1064                if (userContent == null)
1065                {
1066                    userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), availableLanguage.getCode());
1067                }
1068            }
1069        }
1070        return userContent;
1071    }
1072
1073    /**
1074     * Get the members of a project, sorted by managers, non empty role and name
1075     * @param project the project
1076     * @param expandGroup true to expand the user of a group
1077     * @return the members of project
1078     * @throws AmetysRepositoryException if an error occurred
1079     */
1080    public Set<ProjectMember> getProjectMembers(Project project, boolean expandGroup) throws AmetysRepositoryException
1081    {
1082        return getProjectMembers(project, expandGroup, Set.of());
1083    }
1084
1085    /**
1086     * Get the members of a project, sorted by managers, non empty role and name
1087     * @param project the project
1088     * @param expandGroup true to expand the user of a group
1089     * @param defaultSet default set to return when project has no site
1090     * @return the members of project
1091     * @throws AmetysRepositoryException if an error occurred
1092     */
1093    public Set<ProjectMember> getProjectMembers(Project project, boolean expandGroup, Set<ProjectMember> defaultSet) throws AmetysRepositoryException
1094    {
1095        Cache<ProjectMemberCacheKey, Set<ProjectMember>> cache = _getCache();
1096        if (project == null)
1097        {
1098            return defaultSet;
1099        }
1100        ProjectMemberCacheKey cacheKey = ProjectMemberCacheKey.of(project.getId(), expandGroup);
1101        if (cache.hasKey(cacheKey))
1102        {
1103            Set<ProjectMember> projectMembers = cache.get(cacheKey);
1104            return projectMembers != null ? projectMembers : defaultSet;
1105        }
1106        else
1107        {
1108            Set<ProjectMember> projectMembers = _getProjectMembers(project, expandGroup);
1109            cache.put(cacheKey, projectMembers);
1110            return projectMembers != null ? projectMembers : defaultSet;
1111        }
1112    }
1113
1114    private Set<ProjectMember> _getProjectMembers(Project project, boolean expandGroup)
1115    {
1116        Comparator<ProjectMember> managerComparator = Comparator.comparing(m -> m.isManager() ? 0 : 1);
1117        Comparator<ProjectMember> roleComparator = Comparator.comparing(m -> StringUtils.isNotBlank(m.getRole()) ? 0 : 1);
1118        // Use sortable title for sort, and concatenate it with hash code of user, so that homonyms do not appear equals
1119        Comparator<ProjectMember> nameComparator = (m1, m2) -> (m1.getSortableTitle() + m1.hashCode()).compareToIgnoreCase(m2.getSortableTitle() + m2.hashCode());
1120        
1121        Set<ProjectMember> members = new TreeSet<>(managerComparator.thenComparing(roleComparator).thenComparing(nameComparator));
1122        
1123        Map<JCRProjectMember, Object> jcrMembers = getJCRProjectMembers(project);
1124        List<UserIdentity> managers = Arrays.asList(project.getManagers());
1125        
1126        Site site = project.getSite();
1127        if (site == null)
1128        {
1129            getLogger().error("Can not compute members in the project " + project.getName() + " because it can not be linked to an existing site");
1130            return null;
1131        }
1132        String projectSiteName = site.getName();
1133
1134        Set<String> projectGroupDirectory = _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites/" + projectSiteName);
1135        
1136        for (Entry<JCRProjectMember, Object> entry : jcrMembers.entrySet())
1137        {
1138            JCRProjectMember jcrMember = entry.getKey();
1139            if (MemberType.USER == jcrMember.getType())
1140            {
1141                User user = (User) entry.getValue();
1142                boolean isManager = managers.contains(jcrMember.getUser());
1143                
1144                ProjectMember projectMember = new ProjectMember(user, jcrMember.getRole(), isManager);
1145                if (!members.add(projectMember) && _projectManager.isUserInProjectPopulations(project, user.getIdentity()))
1146                {
1147                    //if set already contains the user, override it (users always take over users' group)
1148                    members.remove(projectMember); // remove the one in  the set
1149                    members.add(projectMember); // add the new one
1150                }
1151            }
1152            else if (MemberType.GROUP == jcrMember.getType())
1153            {
1154                Group group = (Group) entry.getValue();
1155                if (projectGroupDirectory.contains(group.getGroupDirectory().getId()))
1156                {
1157                    if (expandGroup)
1158                    {
1159                        for (UserIdentity userIdentity : group.getUsers())
1160                        {
1161                            User user = _userManager.getUser(userIdentity);
1162                            if (user != null && _projectManager.isUserInProjectPopulations(project, userIdentity))
1163                            {
1164                                ProjectMember projectMember = new ProjectMember(user, null, false);
1165                                members.add(projectMember); // add if does not exist yet
1166                            }
1167                        }
1168                    }
1169                    else
1170                    {
1171                        // Add the member as group
1172                        members.add(new ProjectMember(group));
1173                    }
1174                }
1175            }
1176        }
1177        return members;
1178    }
1179    
1180    /**
1181     * Retrieves the rights for the current user in the project
1182     * @param projectName The project Name
1183     * @return The project
1184     */
1185    @Callable
1186    public Map<String, Object> getMemberModuleRights(String projectName)
1187    {
1188        Map<String, Object> results = new HashMap<>();
1189        Map<String, Object> rights = new HashMap<>();
1190        
1191        Project project = _projectManager.getProject(projectName);
1192        if (project == null)
1193        {
1194            results.put("message", "unknown-project");
1195            results.put("success", false);
1196        }
1197        else
1198        {
1199            rights.put("view", _projectRightHelper.canViewMembers(project));
1200            rights.put("add", _projectRightHelper.canAddMember(project));
1201            rights.put("edit", _projectRightHelper.canEditMember(project));
1202            rights.put("delete", _projectRightHelper.canRemoveMember(project));
1203            results.put("rights", rights);
1204            results.put("success", true);
1205        }
1206        
1207        return results;
1208    }
1209    
1210    /**
1211     * Get the list of users of the project
1212     * @param project The project
1213     * @return The list of users
1214     */
1215    public Map<JCRProjectMember, Object> getJCRProjectMembers(Project project)
1216    {
1217        Map<JCRProjectMember, Object> projectMembers = new HashMap<>();
1218        
1219        if (project != null)
1220        {
1221            ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1222            
1223            for (AmetysObject memberNode : membersNode.getChildren())
1224            {
1225                if (memberNode instanceof JCRProjectMember)
1226                {
1227                    JCRProjectMember jCRProjectMember = (JCRProjectMember) memberNode;
1228                    if (jCRProjectMember.getType() == MemberType.USER)
1229                    {
1230                        UserIdentity userIdentity = jCRProjectMember.getUser();
1231                        User user = _userManager.getUser(userIdentity);
1232                        if (user != null)
1233                        {
1234                            projectMembers.put((JCRProjectMember) memberNode, user);
1235                        }
1236                    }
1237                    else
1238                    {
1239                        GroupIdentity groupIdentity = jCRProjectMember.getGroup();
1240                        Group group = _groupManager.getGroup(groupIdentity);
1241                        if (group != null)
1242                        {
1243                            projectMembers.put((JCRProjectMember) memberNode, group);
1244                        }
1245                    }
1246                }
1247            }
1248        }
1249        
1250        return projectMembers;
1251    }
1252    
1253    /**
1254     * Test if an user is a member of a project (directly or by a group)
1255     * @param project The project
1256     * @param userIdentity The user identity
1257     * @return True if this user is a member of this project
1258     */
1259    public boolean isProjectMember(Project project, UserIdentity userIdentity)
1260    {
1261        return getProjectMember(project, userIdentity) != null;
1262    }
1263    
1264    /**
1265     * Retrieve the member of a project corresponding to the user identity
1266     * @param project The project
1267     * @param userIdentity The user identity
1268     * @return The member of this project, which can be of type "user" or "group", or null if the user is not in the project
1269     */
1270    public ProjectMember getProjectMember(Project project, UserIdentity userIdentity)
1271    {
1272        return getProjectMember(project, userIdentity, null);
1273    }
1274    
1275    /**
1276     * Retrieve the member of a project corresponding to the user identity
1277     * @param project The project
1278     * @param userIdentity The user identity
1279     * @param userGroups The user groups. If null the user's groups will be expanded.
1280     * @return The member of this project, which can be of type "user" or "group", or null if the user is not in the project
1281     */
1282    public ProjectMember getProjectMember(Project project, UserIdentity userIdentity, Set<GroupIdentity> userGroups)
1283    {
1284        if (userIdentity == null)
1285        {
1286            return null;
1287        }
1288                
1289        Set<ProjectMember> members = getProjectMembers(project, true);
1290        
1291        ProjectMember projectMember = members.stream()
1292                .filter(member -> MemberType.USER == member.getType())
1293                .filter(member -> userIdentity.equals(member.getUser().getIdentity()))
1294                .findFirst()
1295                .orElse(null);
1296        
1297        if (projectMember != null)
1298        {
1299            return projectMember;
1300        }
1301        
1302        Set<GroupIdentity> groups = userGroups == null ? _groupManager.getUserGroups(userIdentity) : userGroups; // get user's groups
1303
1304        if (!groups.isEmpty())
1305        {
1306            return members.stream()
1307                    .filter(member -> MemberType.GROUP == member.getType())
1308                    .filter(member -> groups.contains(member.getGroup().getIdentity()))
1309                    .findFirst()
1310                    .orElse(null);
1311        }
1312        
1313        return null;
1314    }
1315    
1316    /**
1317     * Set the manager of a project
1318     * @param projectName The project name
1319     * @param profileId The profile id to affect
1320     * @param managers The managers' user identity
1321     */
1322    public void setProjectManager(String projectName, String profileId, List<UserIdentity> managers)
1323    {
1324        Project project = _projectManager.getProject(projectName);
1325        if (project == null)
1326        {
1327            return;
1328        }
1329        
1330        project.setManagers(managers.toArray(new UserIdentity[managers.size()]));
1331        
1332        for (UserIdentity userIdentity : managers)
1333        {
1334            JCRProjectMember member = _getOrCreateJCRProjectMember(project, userIdentity);
1335            
1336            Set<String> allowedProfiles = Set.of(profileId);
1337            for (WorkspaceModule module : _projectManager.getModules(project))
1338            {
1339                setProfileOnModule(member, project, module, allowedProfiles);
1340            }
1341        }
1342        
1343        project.saveChanges();
1344        
1345        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1346        
1347        // Clear rights manager cache (if I remove my own rights)
1348        _rightManager.clearCaches();
1349        
1350//        Request request = ContextHelper.getRequest(_context);
1351//        if (request != null)
1352//        {
1353//            request.removeAttribute(RightManager.CACHE_REQUEST_ATTRIBUTE_NAME);
1354//        }
1355    }
1356    
1357    private void _notifyAclUpdated(UserIdentity userIdentity, AmetysObject aclContext, Collection<String> aclProfiles)
1358    {
1359        Map<String, Object> eventParams = new HashMap<>();
1360        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, aclContext);
1361        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, aclContext.getId());
1362        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, aclProfiles);
1363        eventParams.put(org.ametys.cms.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
1364        
1365        _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, userIdentity, eventParams));
1366    }
1367
1368    /**
1369     * Retrieve or create a user in a project
1370     * @param project The project
1371     * @param userIdentity the user
1372     * @return The user
1373     */
1374    private JCRProjectMember _getOrCreateJCRProjectMember(Project project, UserIdentity userIdentity)
1375    {
1376        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.USER == ((JCRProjectMember) memberNode).getType()
1377                && userIdentity.equals(((JCRProjectMember) memberNode).getUser());
1378        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, findMemberPredicate);
1379        
1380        if (projectMember.needsSave())
1381        {
1382            projectMember.setUser(userIdentity);
1383            projectMember.setType(MemberType.USER);
1384        }
1385        
1386        return projectMember;
1387    }
1388
1389    /**
1390     * Retrieve or create a group as a member in a project
1391     * @param project The project
1392     * @param groupIdentity the group
1393     * @return The user
1394     */
1395    private JCRProjectMember _getOrCreateJCRProjectMember(Project project, GroupIdentity groupIdentity)
1396    {
1397        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.GROUP == ((JCRProjectMember) memberNode).getType()
1398                && groupIdentity.equals(((JCRProjectMember) memberNode).getGroup());
1399        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, findMemberPredicate);
1400        
1401        if (projectMember.needsSave())
1402        {
1403            projectMember.setGroup(groupIdentity);
1404            projectMember.setType(MemberType.GROUP);
1405        }
1406        
1407        return projectMember;
1408    }
1409    
1410    /**
1411     * Retrieve or create a member in a project
1412     * @param project The project
1413     * @param findMemberPredicate The predicate to find the member node
1414     * @return The member node. A new node is created if the member node was not found
1415     */
1416    protected JCRProjectMember _getOrCreateJCRProjectMember(Project project, Predicate<? super AmetysObject> findMemberPredicate)
1417    {
1418        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1419        
1420        Optional<AmetysObject> member = _getProjectMembersNode(project).getChildren()
1421                                                                       .stream()
1422                                                                       .filter(memberNode -> memberNode instanceof JCRProjectMember)
1423                                                                       .filter(findMemberPredicate)
1424                                                                       .findFirst();
1425        if (member.isPresent())
1426        {
1427            return (JCRProjectMember) member.get();
1428        }
1429        
1430        String baseName = "member";
1431        String name = baseName;
1432        int index = 1;
1433        while (membersNode.hasChild(name))
1434        {
1435            index++;
1436            name = baseName + "-" + index;
1437        }
1438        
1439        JCRProjectMember jcrProjectMember = membersNode.createChild(name, __PROJECT_MEMBER_NODE_TYPE);
1440        
1441        // we invalidate the cache has we had to create a new user
1442        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1443        
1444        return jcrProjectMember;
1445    }
1446
1447    
1448    /**
1449     * Remove a user from a project
1450     * @param projectName The project name
1451     * @param identity The identity of the user or group, who must be a member of the project
1452     * @param type The type of the member, user or group
1453     * @return The error code, if an error occurred
1454     */
1455    @Callable
1456    public Map<String, Object> removeMember(String projectName, String identity, String type)
1457    {
1458        return _removeMember(projectName, identity, type, true, true);
1459    }
1460    
1461    private Map<String, Object> _removeMember(String projectName, String identity, String type, boolean checkCurrentUser, boolean chekRights)
1462    {
1463        Map<String, Object> result = new HashMap<>();
1464        
1465        MemberType memberType = MemberType.valueOf(type.toUpperCase());
1466        boolean isTypeUser = MemberType.USER == memberType;
1467        boolean isTypeGroup = MemberType.GROUP == memberType;
1468        UserIdentity user = Optional.ofNullable(identity)
1469                                    .filter(id -> id != null && isTypeUser)
1470                                    .map(UserIdentity::stringToUserIdentity)
1471                                    .orElse(null);
1472        GroupIdentity group = Optional.ofNullable(identity)
1473                                      .filter(id -> id != null && isTypeGroup)
1474                                      .map(GroupIdentity::stringToGroupIdentity)
1475                                      .orElse(null);
1476        
1477        if (isTypeGroup && group == null
1478            || isTypeUser && user == null)
1479        {
1480            result.put("success", false);
1481            result.put("message", isTypeGroup ? "unknown-group" : "unknown-user");
1482            return result;
1483        }
1484        
1485        Project project = _projectManager.getProject(projectName);
1486        if (project == null)
1487        {
1488            result.put("success", false);
1489            result.put("message", "unknown-project");
1490            return result;
1491        }
1492        
1493        if (checkCurrentUser && _isCurrentUser(isTypeUser, user))
1494        {
1495            result.put("success", false);
1496            result.put("message", "current-user");
1497            return result;
1498        }
1499        
1500        // If there is only one manager, do not remove him from the project's members
1501        if (isTypeUser && isOnlyManager(project, user))
1502        {
1503            result.put("success", false);
1504            result.put("message", "only-manager");
1505            return result;
1506        }
1507        
1508        if (chekRights && !_projectRightHelper.canRemoveMember(project))
1509        {
1510            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to remove member without convenient right [" + projectName + ", " + identity + "]");
1511        }
1512        
1513        JCRProjectMember projectMember = null;
1514        if (isTypeUser)
1515        {
1516            projectMember = _getProjectMember(project, user);
1517        }
1518        else if (isTypeGroup)
1519        {
1520            projectMember = _getProjectMember(project, group);
1521        }
1522        
1523        if (projectMember == null)
1524        {
1525            result.put("success", false);
1526            result.put("message", "unknown-member");
1527            return result;
1528        }
1529        
1530        _removeMember(projectMember, project);
1531        
1532        result.put("success", true);
1533        return result;
1534    }
1535
1536    private void _removeMember(JCRProjectMember projectMember, Project project)
1537    {
1538        _removeManager(project, projectMember);
1539        _removeMemberProfiles(project, projectMember);
1540        MemberType memberType = projectMember.getType();
1541        
1542        Map<String, Object> eventParams = new HashMap<>();
1543        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, MemberType.USER.equals(memberType)
1544                ? UserIdentity.userIdentityToString(projectMember.getUser())
1545                        : GroupIdentity.groupIdentityToString(projectMember.getGroup()));
1546        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, memberType);
1547        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1548        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
1549
1550        projectMember.remove();
1551        project.saveChanges();
1552        
1553        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1554        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_DELETED, _currentUserProvider.getUser(), eventParams));
1555    }
1556
1557    private boolean _isCurrentUser(boolean isTypeUser, UserIdentity user)
1558    {
1559        return isTypeUser && _currentUserProvider.getUser().equals(user);
1560    }
1561
1562    /**
1563     * Check if a user is the only manager of a project
1564     * @param project the project
1565     * @param user the user
1566     * @return true if the user is the only manager of the project
1567     */
1568    public boolean isOnlyManager(Project project, UserIdentity user)
1569    {
1570        UserIdentity[] managers = project.getManagers();
1571        return managers.length == 1 && managers[0].equals(user);
1572    }
1573
1574    private JCRProjectMember _getProjectMember(Project project, GroupIdentity group)
1575    {
1576        JCRProjectMember projectMember = null;
1577        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1578        
1579        for (AmetysObject memberNode : membersNode.getChildren())
1580        {
1581            if (memberNode instanceof JCRProjectMember)
1582            {
1583                JCRProjectMember member = (JCRProjectMember) memberNode;
1584                if (MemberType.GROUP == member.getType() && group.equals(member.getGroup()))
1585                {
1586                    projectMember = (JCRProjectMember) memberNode;
1587                }
1588
1589            }
1590        }
1591        return projectMember;
1592    }
1593
1594    private JCRProjectMember _getProjectMember(Project project, UserIdentity user)
1595    {
1596        JCRProjectMember projectMember = null;
1597        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1598        
1599        for (AmetysObject memberNode : membersNode.getChildren())
1600        {
1601            if (memberNode instanceof JCRProjectMember)
1602            {
1603                JCRProjectMember member = (JCRProjectMember) memberNode;
1604                if (MemberType.USER == member.getType() && user.equals(member.getUser()))
1605                {
1606                    projectMember = (JCRProjectMember) memberNode;
1607                }
1608            }
1609        }
1610        return projectMember;
1611    }
1612    
1613    private void _removeManager(Project project, JCRProjectMember projectMember)
1614    {
1615        if (MemberType.USER.equals(projectMember.getType()))
1616        {
1617            UserIdentity user = projectMember.getUser();
1618            UserIdentity[] oldManagers = project.getManagers();
1619            
1620            // Remove the user from the project's managers
1621            UserIdentity[] managers = Arrays.stream(oldManagers)
1622                  .filter(manager -> !manager.equals(user))
1623                  .toArray(UserIdentity[]::new);
1624            
1625            project.setManagers(managers);
1626        }
1627    }
1628
1629    private void _removeMemberProfiles(Project project, JCRProjectMember projectMember)
1630    {
1631        for (WorkspaceModule module : _projectManager.getModules(project))
1632        {
1633            ModifiableResourceCollection moduleRootNode = module.getModuleRoot(project, false);
1634            _removeMemberProfiles(projectMember, moduleRootNode);
1635        }
1636    }
1637    
1638    /**
1639     * Retrieves the users node of the project
1640     * The users node will be created if necessary
1641     * @param project The project
1642     * @return The users node of the project
1643     */
1644    protected ModifiableTraversableAmetysObject _getProjectMembersNode(Project project)
1645    {
1646        if (project == null)
1647        {
1648            throw new AmetysRepositoryException("Error getting the project users node, project is null");
1649        }
1650        
1651        try
1652        {
1653            ModifiableTraversableAmetysObject membersNode;
1654            if (project.hasChild(__PROJECT_MEMBERS_NODE))
1655            {
1656                membersNode = project.getChild(__PROJECT_MEMBERS_NODE);
1657            }
1658            else
1659            {
1660                membersNode = project.createChild(__PROJECT_MEMBERS_NODE, __PROJECT_MEMBERS_NODE_TYPE);
1661            }
1662            
1663            return membersNode;
1664        }
1665        catch (AmetysRepositoryException e)
1666        {
1667            throw new AmetysRepositoryException("Error getting the project users node", e);
1668        }
1669    }
1670    
1671    /**
1672     * Get the JSON representation of a group
1673     * @param group The group
1674     * @return The group
1675     */
1676    protected Map<String, Object> group2Json(Group group)
1677    {
1678        Map<String, Object> infos = new HashMap<>();
1679        infos.put("id", GroupIdentity.groupIdentityToString(group.getIdentity()));
1680        infos.put("groupId", group.getIdentity().getId());
1681        infos.put("label", group.getLabel());
1682        infos.put("sortablename", group.getLabel());
1683        infos.put("groupDirectory", group.getIdentity().getDirectoryId());
1684        return infos;
1685    }
1686
1687    /**
1688     * Count the total of unique users in the project and in the project's group
1689     * @param project The project
1690     * @return The total of members
1691     */
1692    public Long getMembersCount(Project project)
1693    {
1694        Set<ProjectMember> projectMembers = getProjectMembers(project, true);
1695                
1696        return (long) projectMembers.size();
1697    }
1698
1699    /**
1700     * Get the users from a group that are part of the project. They can be filtered with a predicate
1701     * @param group The group
1702     * @param project The project
1703     * @param filteringPredicate The predicate to filter
1704     * @return The list of users
1705     */
1706    public List<User> getGroupUsersFromProject(Group group, Project project, BiPredicate<Project, UserIdentity> filteringPredicate)
1707    {
1708        Set<String> projectPopulations = _populationContextHelper.getUserPopulationsOnContexts(List.of("/sites/" + project.getSite().getName()), false, false);
1709        
1710        return group.getUsers().stream()
1711                .filter(user -> projectPopulations.contains(user.getPopulationId()))
1712                .filter(user -> filteringPredicate.test(project, user))
1713                .map(_userManager::getUser)
1714                .filter(Objects::nonNull)
1715                .collect(Collectors.toList());
1716    }
1717
1718    /**
1719     * Make the current user leave the project
1720     * @param projectName The project name
1721     * @return The error code, if an error occurred
1722     */
1723    @Callable
1724    public Map<String, Object> leaveProject(String projectName)
1725    {
1726        UserIdentity currentUser = _currentUserProvider.getUser();
1727        String identity = UserIdentity.userIdentityToString(currentUser);
1728        
1729        Map<String, Object> result = _removeMember(projectName, identity, MemberType.USER.toString(), false, false);
1730        
1731        Project project = _projectManager.getProject(projectName);
1732        result.put("project", _projectsCatalogueManager.detailedProject2json(project));
1733        
1734        return result;
1735    }
1736    
1737    private Cache<ProjectMemberCacheKey, Set<ProjectMember>> _getCache()
1738    {
1739        return _abstractCacheManager.get(__PROJECT_MEMBER_CACHE);
1740    }
1741    
1742    /**
1743     * This class represents a member of a project. Could be a user or a group
1744     *
1745     */
1746    public static class ProjectMember
1747    {
1748        private String _title;
1749        private String _sortableTitle;
1750        private MemberType _type;
1751        private String _role;
1752        private User _user;
1753        private Group _group;
1754        private boolean _isManager;
1755        
1756        /**
1757         * Create a project member as a group
1758         * @param group the group attached to this member. Cannot be null.
1759         */
1760        public ProjectMember(Group group)
1761        {
1762            _title = group.getLabel();
1763            _sortableTitle = group.getLabel();
1764            _type = MemberType.GROUP;
1765            _role = null;
1766            _isManager = false;
1767            _user = null;
1768            _group = group;
1769        }
1770        
1771        /**
1772         * Create a project member as a group
1773         * @param role the role
1774         * @param isManager true if the member is a manager of the project
1775         * @param user the user attached to this member. Cannot be null.
1776         */
1777        public ProjectMember(User user, String role, boolean isManager)
1778        {
1779            _title = user.getFullName();
1780            _sortableTitle = user.getSortableName();
1781            _type = MemberType.USER;
1782            _role = role;
1783            _isManager = isManager;
1784            _user = user;
1785            _group = null;
1786        }
1787        
1788        /**
1789         * Get the title of the member.
1790         * @return The title of the member
1791         */
1792        public String getTitle()
1793        {
1794            return _title;
1795        }
1796        
1797        /**
1798         * Get the sortable title of the member.
1799         * @return The sortable title of the member
1800         */
1801        public String getSortableTitle()
1802        {
1803            return _sortableTitle;
1804        }
1805
1806        /**
1807         * Get the type of the member. It can be a user or a group
1808         * @return The type of the member
1809         */
1810        public MemberType getType()
1811        {
1812            return _type;
1813        }
1814
1815        /**
1816         * Get the role of the member.
1817         * @return The role of the member
1818         */
1819        public String getRole()
1820        {
1821            return _role;
1822        }
1823
1824        /**
1825         * Test if the member is a manager of the project
1826         * @return True if this user is a manager of the project
1827         */
1828        public boolean isManager()
1829        {
1830            return _isManager;
1831        }
1832        
1833        /**
1834         * Get the user of the member.
1835         * @return The user of the member
1836         */
1837        public User getUser()
1838        {
1839            return _user;
1840        }
1841        
1842        /**
1843         * Get the group of the member.
1844         * @return The group of the member
1845         */
1846        public Group getGroup()
1847        {
1848            return _group;
1849        }
1850        
1851        @Override
1852        public boolean equals(Object obj)
1853        {
1854            if (obj == null || !(obj instanceof ProjectMember))
1855            {
1856                return false;
1857            }
1858            
1859            ProjectMember otherMember = (ProjectMember) obj;
1860            
1861            if (getType() != otherMember.getType())
1862            {
1863                return false;
1864            }
1865            
1866            if (getType() == MemberType.USER)
1867            {
1868                return getUser().equals(otherMember.getUser());
1869            }
1870            else
1871            {
1872                return getGroup().equals(otherMember.getGroup());
1873            }
1874        }
1875        
1876        @Override
1877        public int hashCode()
1878        {
1879            return getType() == MemberType.USER ? getUser().getIdentity().hashCode() : getGroup().getIdentity().hashCode();
1880        }
1881    }
1882    
1883    private static final class ProjectMemberCacheKey extends AbstractCacheKey
1884    {
1885        public ProjectMemberCacheKey(String projectId, Boolean extendGroup)
1886        {
1887            super(projectId, extendGroup);
1888        }
1889        
1890        public static ProjectMemberCacheKey of(String projectId, Boolean withExpandedGroup)
1891        {
1892            return new ProjectMemberCacheKey(projectId, withExpandedGroup);
1893        }
1894    }
1895    
1896    public int getPriority()
1897    {
1898        return Integer.MAX_VALUE;
1899    }
1900    
1901    public boolean supports(Event event)
1902    {
1903        String eventId = event.getId();
1904        return org.ametys.core.ObservationConstants.EVENT_USER_DELETED.equals(eventId)
1905            || org.ametys.core.ObservationConstants.EVENT_GROUP_DELETED.equals(eventId);
1906    }
1907    
1908    public void observe(Event event, Map<String, Object> transientVars) throws Exception
1909    {
1910        // handle manually the removal of member nodes.
1911        // All method providing access to project member needs to resolve the User or Group
1912        String query;
1913        switch (event.getId())
1914        {
1915            case org.ametys.core.ObservationConstants.EVENT_USER_DELETED:
1916                UserIdentity userIdentity = (UserIdentity) event.getArguments().get(org.ametys.core.ObservationConstants.ARGS_USER);
1917                query = "//element(*, " + JCRProjectMemberFactory.MEMBER_NODETYPE + ")["
1918                        + new UserExpression(JCRProjectMember.METADATA_USER, Operator.EQ, userIdentity).build() + "]";
1919                break;
1920            case org.ametys.core.ObservationConstants.EVENT_GROUP_DELETED:
1921                GroupIdentity groupIdentity = (GroupIdentity) event.getArguments().get(org.ametys.core.ObservationConstants.ARGS_GROUP);
1922                query = "//element(*, " + JCRProjectMemberFactory.MEMBER_NODETYPE + ")["
1923                    + JCRProjectMember.METADATA_GROUP + "/" + JCRProjectMember.METADATA_GROUP_ID + "=" + groupIdentity.getId()
1924                    + " and " + JCRProjectMember.METADATA_GROUP + "/" + JCRProjectMember.METADATA_GROUP_DIRECTORY + "=" + groupIdentity.getDirectoryId()
1925                    + "]";
1926                break;
1927            default:
1928                throw new IllegalStateException("Event id '" + event.getId() + "' is not supported");
1929        }
1930        
1931        try (AmetysObjectIterable<JCRProjectMember> members = _resolver.query(query))
1932        {
1933            for (JCRProjectMember member: members)
1934            {
1935                AmetysObject parent = member.getParent().getParent();
1936                if (parent instanceof Project project)
1937                {
1938                    _removeMember(member, project);
1939                }
1940            }
1941        }
1942    }
1943}