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