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 ? getOrCreateProjectMember(project, user) : getOrCreateProjectMember(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 JCRProjectMember projectMember = isTypeUser ? getOrCreateProjectMember(project, user) : getOrCreateProjectMember(project, group); 501 boolean newMember = projectMember.needsSave(); 502 503 if (role != null) 504 { 505 projectMember.setRole(role); 506 } 507 508 setMemberProfiles(newProfiles, projectMember, project, newMember); 509 510 project.saveChanges(); 511 512 if (newMember || role != null) 513 { 514 _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null)); 515 } 516 517 if (newMember) 518 { 519 // Notify listeners 520 Map<String, Object> eventParams = new HashMap<>(); 521 eventParams.put(ObservationConstants.ARGS_MEMBER, projectMember); 522 eventParams.put(ObservationConstants.ARGS_MEMBER_ID, projectMember.getId()); 523 eventParams.put(ObservationConstants.ARGS_PROJECT, project); 524 eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId()); 525 eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, identity); 526 eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, projectMember.getType()); 527 _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_ADDED, _currentUserProvider.getUser(), eventParams)); 528 } 529 530 result.put("success", true); 531 return result; 532 } 533 534 /** 535 * Add a user to a project with open inscriptions, using the default values 536 * @param project The project 537 * @param user The user 538 * @return True if the user was successfully added 539 */ 540 public boolean addProjectMember(Project project, UserIdentity user) 541 { 542 if (user == null) 543 { 544 return false; 545 } 546 547 InscriptionStatus inscriptionStatus = project.getInscriptionStatus(); 548 if (!inscriptionStatus.equals(InscriptionStatus.OPEN)) 549 { 550 return false; 551 } 552 553 JCRProjectMember projectMember = getOrCreateProjectMember(project, user); 554 555 Set<String> allowedProfiles = Set.of(project.getDefaultProfile()); 556 for (WorkspaceModule module : _moduleManagerEP.getModules()) 557 { 558 _setProfileOnModule(projectMember, project, module, allowedProfiles); 559 } 560 561 project.saveChanges(); 562 563 _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null)); 564 565 // Notify listeners 566 Map<String, Object> eventParams = new HashMap<>(); 567 eventParams.put(ObservationConstants.ARGS_MEMBER, projectMember); 568 eventParams.put(ObservationConstants.ARGS_MEMBER_ID, projectMember.getId()); 569 eventParams.put(ObservationConstants.ARGS_PROJECT, project); 570 eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId()); 571 eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, UserIdentity.userIdentityToString(user)); 572 eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, MemberType.USER); 573 _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_ADDED, _currentUserProvider.getUser(), eventParams)); 574 575 return true; 576 } 577 578 /** 579 * Set the profiles for a member 580 * @param newProfiles The allowed profile by module 581 * @param projectMember The member 582 * @param project The project 583 * @param newMember true if it is a new created member 584 */ 585 public void setMemberProfiles(Map<String, String> newProfiles, JCRProjectMember projectMember, Project project, boolean newMember) 586 { 587 for (Map.Entry<String, String> entry : newProfiles.entrySet()) 588 { 589 String moduleId = entry.getKey(); 590 String profileId = entry.getValue(); 591 592 WorkspaceModule module = _moduleManagerEP.getModule(moduleId); 593 _setProfileOnModule(projectMember, project, module, profileId != null ? Set.of(profileId) : Set.of()); 594 } 595 } 596 597 private void _setProfileOnModule(JCRProjectMember member, Project project, WorkspaceModule module, Set<String> allowedProfiles) 598 { 599 if (module != null && _projectManager.isModuleActivated(project, module.getId())) 600 { 601 AmetysObject moduleObject = module.getModuleRoot(project, false); 602 _setMemberProfiles(member, allowedProfiles, moduleObject); 603 } 604 } 605 606 private Set<String> _getAllowedProfile(JCRProjectMember member, AmetysObject object) 607 { 608 if (MemberType.GROUP == member.getType()) 609 { 610 Map<GroupIdentity, Map<UserOrGroup, Set<String>>> profilesForGroups = _profileAssignmentStorageExtensionPoint.getProfilesForGroups(object, Set.of(member.getGroup())); 611 return Optional.ofNullable(profilesForGroups.get(member.getGroup())).map(a -> a.get(UserOrGroup.ALLOWED)).orElse(Set.of()); 612 } 613 else 614 { 615 Map<UserIdentity, Map<UserOrGroup, Set<String>>> profilesForUsers = _profileAssignmentStorageExtensionPoint.getProfilesForUsers(object, member.getUser()); 616 return Optional.ofNullable(profilesForUsers.get(member.getUser())).map(a -> a.get(UserOrGroup.ALLOWED)).orElse(Set.of()); 617 } 618 } 619 620 private void _setMemberProfiles(JCRProjectMember member, Set<String> allowedProfiles, AmetysObject object) 621 { 622 Set<String> currentAllowedProfiles = _getAllowedProfile(member, object); 623 624 Collection<String> profilesToRemove = CollectionUtils.removeAll(currentAllowedProfiles, allowedProfiles); 625 626 Collection<String> profilesToAdd = CollectionUtils.removeAll(allowedProfiles, currentAllowedProfiles); 627 628 for (String profileId : profilesToRemove) 629 { 630 _removeProfile(member, profileId, object); 631 } 632 633 for (String profileId : profilesToAdd) 634 { 635 _addProfile(member, profileId, object); 636 } 637 638 Collection<String> updatedProfiles = CollectionUtils.union(profilesToAdd, profilesToRemove); 639 640 if (updatedProfiles.size() > 0) 641 { 642 _notifyAclUpdated(_currentUserProvider.getUser(), object, updatedProfiles); 643 } 644 } 645 646 private void _removeProfile(JCRProjectMember member, String profileId, AmetysObject aclObject) 647 { 648 if (MemberType.GROUP == member.getType()) 649 { 650 _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), profileId, aclObject); 651 } 652 else 653 { 654 _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), profileId, aclObject); 655 } 656 } 657 658 private void _addProfile(JCRProjectMember member, String profileId, AmetysObject aclObject) 659 { 660 if (MemberType.GROUP == member.getType()) 661 { 662 _profileAssignmentStorageExtensionPoint.allowProfileToGroup(member.getGroup(), profileId, aclObject); 663 } 664 else 665 { 666 _profileAssignmentStorageExtensionPoint.allowProfileToUser(member.getUser(), profileId, aclObject); 667 } 668 } 669 670 private void _removeMemberProfiles(JCRProjectMember member, AmetysObject object) 671 { 672 Set<String> currentAllowedProfiles = _getAllowedProfile(member, object); 673 674 for (String allowedProfile : currentAllowedProfiles) 675 { 676 _removeProfile(member, allowedProfile, object); 677 } 678 679 if (currentAllowedProfiles.size() > 0) 680 { 681 ((ModifiableAmetysObject) object).saveChanges(); 682 683 Map<String, Object> eventParams = new HashMap<>(); 684 eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, object); 685 eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, object.getId()); 686 eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, currentAllowedProfiles); 687 eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true); 688 689 _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams)); 690 } 691 } 692 693 /** 694 * Get the current user information 695 * @return The user 696 */ 697 @Callable 698 public Map<String, Object> getCurrentUser() 699 { 700 Map<String, Object> result = new HashMap<>(); 701 result.put("user", _userHelper.user2json(_currentUserProvider.getUser())); 702 return result; 703 } 704 705 /** 706 * Get the members of current project or all the members of all projects in where is no current project 707 * @return The members 708 */ 709 @Callable 710 public Map<String, Object> getProjectMembers() 711 { 712 Map<String, Object> result = new HashMap<>(); 713 714 Request request = ContextHelper.getRequest(_context); 715 String projectName = (String) request.getAttribute("projectName"); 716 717 Collection<Project> projects = new ArrayList<>(); 718 719 if (StringUtils.isNotEmpty(projectName)) 720 { 721 projects.add(_projectManager.getProject(projectName)); 722 } 723 else 724 { 725 _projectManager.getProjects() 726 .stream() 727 .forEach(project -> projects.add(project)); 728 } 729 730 result.put("users", projects.stream() 731 .map(project -> getProjectMembers(project, true, false)) 732 .flatMap(Collection::stream) 733 .map(ProjectMember::getUser) 734 .distinct() 735 .map(user -> _userHelper.user2json(user, true)) 736 .collect(Collectors.toList())); 737 738 return result; 739 } 740 741 /** 742 * Get the members of a project, sorted by managers, non empty role and name 743 * @param projectName the project's name 744 * @param lang the language to get user content 745 * @return the members of project 746 * @throws IllegalAccessException if an error occurred 747 * @throws AmetysRepositoryException if an error occurred 748 */ 749 @Callable 750 public Map<String, Object> getProjectMembers(String projectName, String lang) throws IllegalAccessException, AmetysRepositoryException 751 { 752 Map<String, Object> result = new HashMap<>(); 753 754 Project project = _projectManager.getProject(projectName); 755 if (project == null) 756 { 757 result.put("message", "unknow-project"); 758 result.put("success", false); 759 return result; 760 } 761 if (!_projectRightHelper.hasReadAccess(project)) 762 { 763 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without reader right in the project " + project.getPath()); 764 } 765 766 List<Map<String, Object>> membersData = new ArrayList<>(); 767 768 Collection<ProjectMember> projectMembers = getProjectMembers(project, false, true); 769 770 for (ProjectMember projectMember : projectMembers) 771 { 772 Map<String, Object> memberData = new HashMap<>(); 773 774 memberData.put("type", projectMember.getType().name().toLowerCase()); 775 memberData.put("title", projectMember.getTitle()); 776 memberData.put("sortabletitle", projectMember.getSortableTitle()); 777 memberData.put("manager", projectMember.isManager()); 778 779 String role = projectMember.getRole(); 780 if (StringUtils.isNotEmpty(role)) 781 { 782 memberData.put("role", role); 783 } 784 785 User user = projectMember.getUser(); 786 if (user != null) 787 { 788 memberData.put("id", UserIdentity.userIdentityToString(user.getIdentity())); 789 memberData.putAll(_userHelper.user2json(user)); 790 791 Content userContent = _getUserContent(lang, user); 792 793 if (userContent != null) 794 { 795 if (userContent.hasNonEmptyValue("function")) 796 { 797 memberData.put("function", userContent.getValue("function")); 798 } 799 800 if (userContent.hasNonEmptyValue("organisation-accronym")) 801 { 802 memberData.put("organisationAcronym", userContent.getValue("organisation-accronym")); 803 } 804 String usersDirectorySiteName = _projectManager.getUsersDirectorySiteName(); 805 String[] contentTypes = userContent.getTypes(); 806 for (String contentType : contentTypes) 807 { 808 // Try to see if a user page exists for this content type 809 UserPage userPage = _userDirectoryPageResolver.getUserPage(userContent, usersDirectorySiteName, lang, contentType); 810 if (userPage != null) 811 { 812 memberData.put("link", _uriResolver.getResolverForType("page").resolve(userPage.getId(), false, true, false)); 813 } 814 } 815 816 } 817 else if (getLogger().isDebugEnabled()) 818 { 819 getLogger().debug("User content not found for user : " + user); 820 } 821 } 822 823 Group group = projectMember.getGroup(); 824 if (group != null) 825 { 826 memberData.putAll(group2Json(group)); 827 } 828 829 membersData.add(memberData); 830 } 831 832 result.put("members", membersData); 833 result.put("success", true); 834 835 return result; 836 } 837 838 private Content _getUserContent(String lang, User user) 839 { 840 Content userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), lang); 841 842 if (userContent == null) 843 { 844 userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), "en"); 845 } 846 847 if (userContent == null) 848 { 849 Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages(); 850 for (Language availableLanguage : availableLanguages.values()) 851 { 852 if (userContent == null) 853 { 854 userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), availableLanguage.getCode()); 855 } 856 } 857 } 858 return userContent; 859 } 860 861 /** 862 * Get the members of a project, sorted by managers, non empty role and name 863 * @param project the project 864 * @param expandGroup true to expand the user of a group 865 * @param legacyArguments this is a legacy arguments depreciated in 4.5 version of the class 866 * @return the members of project 867 * @throws AmetysRepositoryException if an error occurred 868 */ 869 public Collection<ProjectMember> getProjectMembers(Project project, boolean expandGroup, @Deprecated boolean legacyArguments) throws AmetysRepositoryException 870 { 871 Cache<ProjectMemberCacheKey, Set<ProjectMember>> cache = _getCache(); 872 873 ProjectMemberCacheKey cacheKey = ProjectMemberCacheKey.of(project.getId(), expandGroup); 874 if (cache.hasKey(cacheKey)) 875 { 876 return cache.get(cacheKey); 877 } 878 else 879 { 880 Set<ProjectMember> projectMembers = _getProjectMembers(project, expandGroup); 881 cache.put(cacheKey, projectMembers); 882 return projectMembers; 883 } 884 } 885 886 private Set<ProjectMember> _getProjectMembers(Project project, boolean expandGroup) 887 { 888 Comparator<ProjectMember> managerComparator = Comparator.comparing(m -> m.isManager() ? 0 : 1); 889 Comparator<ProjectMember> roleComparator = Comparator.comparing(m -> StringUtils.isNotBlank(m.getRole()) ? 0 : 1); 890 Comparator<ProjectMember> nameComparator = (m1, m2) -> m1.getSortableTitle().compareToIgnoreCase(m2.getSortableTitle()); 891 892 Set<ProjectMember> members = new TreeSet<>(managerComparator.thenComparing(roleComparator).thenComparing(nameComparator)); 893 894 Map<JCRProjectMember, Object> jcrMembers = _getProjectMembers(project); 895 List<UserIdentity> managers = Arrays.asList(project.getManagers()); 896 897 Set<String> projectPopulations = project.getSites().stream() 898 .map(site -> _populationContextHelper.getUserPopulationsOnContext("/sites/" + site.getName(), false)) 899 .flatMap(Set::stream) 900 .collect(Collectors.toSet()); 901 902 Set<String> projectGroupDirectory = project.getSites().stream() 903 .map(site -> _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites/" + site.getName())) 904 .flatMap(Set::stream) 905 .collect(Collectors.toSet()); 906 907 for (Entry<JCRProjectMember, Object> entry : jcrMembers.entrySet()) 908 { 909 JCRProjectMember jcrMember = entry.getKey(); 910 if (MemberType.USER == jcrMember.getType()) 911 { 912 User user = (User) entry.getValue(); 913 boolean isManager = managers.contains(jcrMember.getUser()); 914 915 ProjectMember projectMember = new ProjectMember(user.getFullName(), user.getSortableName(), user, jcrMember.getRole(), isManager); 916 if (!members.add(projectMember) && projectPopulations.contains(user.getIdentity().getPopulationId())) 917 { 918 //if set already contains the user, override it (users always take over users' group) 919 members.remove(projectMember); // remove the one in the set 920 members.add(projectMember); // add the new one 921 } 922 } 923 else if (MemberType.GROUP == jcrMember.getType()) 924 { 925 Group group = (Group) entry.getValue(); 926 if (projectGroupDirectory.contains(group.getGroupDirectory().getId())) 927 { 928 if (expandGroup) 929 { 930 for (UserIdentity userIdentity : group.getUsers()) 931 { 932 User user = _userManager.getUser(userIdentity); 933 if (user != null && projectPopulations.contains(user.getIdentity().getPopulationId())) 934 { 935 ProjectMember projectMember = new ProjectMember(user.getFullName(), user.getSortableName(), user, null, false); 936 members.add(projectMember); // add if does not exist yet 937 } 938 } 939 } 940 else 941 { 942 // Add the member as group 943 members.add(new ProjectMember(group.getLabel(), group.getLabel(), group)); 944 } 945 } 946 } 947 } 948 return members; 949 } 950 951 /** 952 * Retrieves the rights for the current user in the project 953 * @param projectName The project Name 954 * @return The project 955 */ 956 @Callable 957 public Map<String, Object> getMemberModuleRights(String projectName) 958 { 959 Map<String, Object> results = new HashMap<>(); 960 Map<String, Object> rights = new HashMap<>(); 961 962 Project project = _projectManager.getProject(projectName); 963 if (project == null) 964 { 965 results.put("message", "unknow-project"); 966 results.put("success", false); 967 } 968 else 969 { 970 rights.put("view", _projectRightHelper.canViewMembers(project)); 971 rights.put("add", _projectRightHelper.canAddMember(project)); 972 rights.put("edit", _projectRightHelper.canEditMember(project)); 973 rights.put("delete", _projectRightHelper.canRemoveMember(project)); 974 results.put("rights", rights); 975 results.put("success", true); 976 } 977 978 return results; 979 } 980 981 /** 982 * Get the list of users of the project 983 * @param project The project 984 * @return The list of users 985 */ 986 protected Map<JCRProjectMember, Object> _getProjectMembers(Project project) 987 { 988 Map<JCRProjectMember, Object> projectMembers = new HashMap<>(); 989 990 if (project != null) 991 { 992 ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project); 993 994 for (AmetysObject memberNode : membersNode.getChildren()) 995 { 996 if (memberNode instanceof JCRProjectMember) 997 { 998 JCRProjectMember jCRProjectMember = (JCRProjectMember) memberNode; 999 if (jCRProjectMember.getType() == MemberType.USER) 1000 { 1001 UserIdentity userIdentity = jCRProjectMember.getUser(); 1002 User user = _userManager.getUser(userIdentity); 1003 if (user != null) 1004 { 1005 projectMembers.put((JCRProjectMember) memberNode, user); 1006 } 1007 } 1008 else 1009 { 1010 GroupIdentity groupIdentity = jCRProjectMember.getGroup(); 1011 Group group = _groupManager.getGroup(groupIdentity); 1012 if (group != null) 1013 { 1014 projectMembers.put((JCRProjectMember) memberNode, group); 1015 } 1016 } 1017 } 1018 } 1019 } 1020 1021 return projectMembers; 1022 } 1023 1024 /** 1025 * Test if an user is a member of a project (directly or by a group) 1026 * @param project The project 1027 * @param userIdentity The user identity 1028 * @return True if this user is a member of this project 1029 */ 1030 public boolean isProjectMember(Project project, UserIdentity userIdentity) 1031 { 1032 return getProjectMember(project, userIdentity) != null; 1033 } 1034 1035 /** 1036 * Retrieve the member of a project corresponding to the user identity 1037 * @param project The project 1038 * @param userIdentity The user identity 1039 * @return The member of this project, which can be of type "user" or "group", or null if the user is not in the project 1040 */ 1041 public ProjectMember getProjectMember(Project project, UserIdentity userIdentity) 1042 { 1043 return getProjectMember(project, userIdentity, null); 1044 } 1045 1046 /** 1047 * Retrieve the member of a project corresponding to the user identity 1048 * @param project The project 1049 * @param userIdentity The user identity 1050 * @param userGroups The user groups. If null the user's groups will be expanded. 1051 * @return The member of this project, which can be of type "user" or "group", or null if the user is not in the project 1052 */ 1053 public ProjectMember getProjectMember(Project project, UserIdentity userIdentity, Set<GroupIdentity> userGroups) 1054 { 1055 if (userIdentity == null) 1056 { 1057 return null; 1058 } 1059 1060 Collection<ProjectMember> members = getProjectMembers(project, true, false); 1061 1062 ProjectMember projectMember = members.stream() 1063 .filter(member -> MemberType.USER == member.getType()) 1064 .filter(member -> userIdentity.equals(member.getUser().getIdentity())) 1065 .findFirst() 1066 .orElse(null); 1067 1068 if (projectMember != null) 1069 { 1070 return projectMember; 1071 } 1072 1073 Set<GroupIdentity> groups = userGroups == null ? _groupManager.getUserGroups(userIdentity) : userGroups; // get user's groups 1074 1075 if (!groups.isEmpty()) 1076 { 1077 return members.stream() 1078 .filter(member -> MemberType.GROUP == member.getType()) 1079 .filter(member -> groups.contains(member.getGroup().getIdentity())) 1080 .findFirst() 1081 .orElse(null); 1082 } 1083 1084 return null; 1085 } 1086 1087 /** 1088 * Set the manager of a project 1089 * @param projectName The project name 1090 * @param profileId The profile id to affect 1091 * @param managers The managers' user identity 1092 */ 1093 public void setProjectManager(String projectName, String profileId, List<UserIdentity> managers) 1094 { 1095 Project project = _projectManager.getProject(projectName); 1096 if (project == null) 1097 { 1098 return; 1099 } 1100 1101 project.setManagers(managers.toArray(new UserIdentity[managers.size()])); 1102 1103 for (UserIdentity userIdentity : managers) 1104 { 1105 JCRProjectMember member = getOrCreateProjectMember(project, userIdentity); 1106 1107 Set<String> allowedProfiles = Set.of(profileId); 1108 for (WorkspaceModule module : _projectManager.getModules(project)) 1109 { 1110 _setProfileOnModule(member, project, module, allowedProfiles); 1111 } 1112 } 1113 1114 project.saveChanges(); 1115 1116 _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null)); 1117 1118 // Clear rights manager cache (if I remove my own rights) 1119 _rightManager.clearCaches(); 1120 1121// Request request = ContextHelper.getRequest(_context); 1122// if (request != null) 1123// { 1124// request.removeAttribute(RightManager.CACHE_REQUEST_ATTRIBUTE_NAME); 1125// } 1126 } 1127 1128 private void _notifyAclUpdated(UserIdentity userIdentity, AmetysObject aclContext, Collection<String> aclProfiles) 1129 { 1130 Map<String, Object> eventParams = new HashMap<>(); 1131 eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, aclContext); 1132 eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, aclContext.getId()); 1133 eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, aclProfiles); 1134 eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true); 1135 1136 _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, userIdentity, eventParams)); 1137 } 1138 1139 /** 1140 * Retrieve or create a user in a project 1141 * @param project The project 1142 * @param userIdentity the user 1143 * @return The user 1144 */ 1145 public JCRProjectMember getOrCreateProjectMember(Project project, UserIdentity userIdentity) 1146 { 1147 Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.USER == ((JCRProjectMember) memberNode).getType() 1148 && userIdentity.equals(((JCRProjectMember) memberNode).getUser()); 1149 JCRProjectMember projectMember = _getOrCreateProjectMember(project, findMemberPredicate); 1150 1151 if (projectMember.needsSave()) 1152 { 1153 projectMember.setUser(userIdentity); 1154 projectMember.setType(MemberType.USER); 1155 } 1156 1157 return projectMember; 1158 } 1159 1160 /** 1161 * Retrieve or create a group as a member in a project 1162 * @param project The project 1163 * @param groupIdentity the group 1164 * @return The user 1165 */ 1166 public JCRProjectMember getOrCreateProjectMember(Project project, GroupIdentity groupIdentity) 1167 { 1168 Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.GROUP == ((JCRProjectMember) memberNode).getType() 1169 && groupIdentity.equals(((JCRProjectMember) memberNode).getGroup()); 1170 JCRProjectMember projectMember = _getOrCreateProjectMember(project, findMemberPredicate); 1171 1172 if (projectMember.needsSave()) 1173 { 1174 projectMember.setGroup(groupIdentity); 1175 projectMember.setType(MemberType.GROUP); 1176 } 1177 1178 return projectMember; 1179 } 1180 1181 /** 1182 * Retrieve or create a member in a project 1183 * @param project The project 1184 * @param findMemberPredicate The predicate to find the member node 1185 * @return The member node. A new node is created if the member node was not found 1186 */ 1187 protected JCRProjectMember _getOrCreateProjectMember(Project project, Predicate<? super AmetysObject> findMemberPredicate) 1188 { 1189 ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project); 1190 1191 Optional<AmetysObject> member = _getProjectMembersNode(project).getChildren() 1192 .stream() 1193 .filter(memberNode -> memberNode instanceof JCRProjectMember) 1194 .filter(findMemberPredicate) 1195 .findFirst(); 1196 if (member.isPresent()) 1197 { 1198 return (JCRProjectMember) member.get(); 1199 } 1200 1201 String baseName = "member"; 1202 String name = baseName; 1203 int index = 1; 1204 while (membersNode.hasChild(name)) 1205 { 1206 index++; 1207 name = baseName + "-" + index; 1208 } 1209 1210 JCRProjectMember jcrProjectMember = membersNode.createChild(name, __PROJECT_MEMBER_NODE_TYPE); 1211 1212 // we invalidate the cache has we had to create a new user 1213 _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null)); 1214 1215 return jcrProjectMember; 1216 } 1217 1218 1219 /** 1220 * Remove a user from a project 1221 * @param projectName The project name 1222 * @param identity The identity of the user or group, who must be a member of the project 1223 * @param type The type of the member, user or group 1224 * @return The error code, if an error occurred 1225 * @throws IllegalAccessException If the user cannot execute this operation 1226 */ 1227 @Callable 1228 public Map<String, Object> removeMember(String projectName, String identity, String type) throws IllegalAccessException 1229 { 1230 Map<String, Object> result = new HashMap<>(); 1231 1232 MemberType memberType = MemberType.valueOf(type.toUpperCase()); 1233 boolean isTypeUser = MemberType.USER == memberType; 1234 boolean isTypeGroup = MemberType.GROUP == memberType; 1235 UserIdentity user = Optional.ofNullable(identity) 1236 .filter(id -> id != null && isTypeUser) 1237 .map(UserIdentity::stringToUserIdentity) 1238 .orElse(null); 1239 GroupIdentity group = Optional.ofNullable(identity) 1240 .filter(id -> id != null && isTypeGroup) 1241 .map(GroupIdentity::stringToGroupIdentity) 1242 .orElse(null); 1243 1244 if (isTypeGroup && group == null || isTypeUser && user == null) 1245 { 1246 result.put("success", false); 1247 result.put("message", isTypeGroup ? "unknow-group" : "unknow-user"); 1248 return result; 1249 } 1250 1251 Project project = _projectManager.getProject(projectName); 1252 if (project == null) 1253 { 1254 result.put("success", false); 1255 result.put("message", "unknow-project"); 1256 return result; 1257 } 1258 1259 if (_isCurrentUser(isTypeUser, user)) 1260 { 1261 result.put("success", false); 1262 result.put("message", "current-user"); 1263 return result; 1264 } 1265 1266 // If there is only one manager, do not remove him from the project's members 1267 if (_isOnlyManager(project, isTypeUser, user)) 1268 { 1269 result.put("success", false); 1270 result.put("message", "only-manager"); 1271 return result; 1272 } 1273 1274 if (!_projectRightHelper.canRemoveMember(project)) 1275 { 1276 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to remove member without convenient right [" + projectName + ", " + identity + "]"); 1277 } 1278 1279 JCRProjectMember projectMember = null; 1280 if (isTypeUser) 1281 { 1282 projectMember = _getProjectMember(project, user); 1283 } 1284 else if (isTypeGroup) 1285 { 1286 projectMember = _getProjectMember(project, group); 1287 } 1288 1289 if (projectMember == null) 1290 { 1291 result.put("success", false); 1292 result.put("message", "unknow-member"); 1293 return result; 1294 } 1295 1296 _removeManager(project, isTypeUser, user); 1297 _removeMemberProfiles(project, projectMember); 1298 1299 projectMember.remove(); 1300 project.saveChanges(); 1301 1302 _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null)); 1303 1304 Map<String, Object> eventParams = new HashMap<>(); 1305 eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, identity); 1306 eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, memberType); 1307 eventParams.put(ObservationConstants.ARGS_PROJECT, project); 1308 _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_DELETED, _currentUserProvider.getUser(), eventParams)); 1309 1310 result.put("success", true); 1311 return result; 1312 } 1313 1314 private boolean _isCurrentUser(boolean isTypeUser, UserIdentity user) 1315 { 1316 return isTypeUser && _currentUserProvider.getUser().equals(user); 1317 } 1318 1319 private boolean _isOnlyManager(Project project, boolean isTypeUser, UserIdentity user) 1320 { 1321 UserIdentity[] managers = project.getManagers(); 1322 return isTypeUser && managers.length == 1 && managers[0].equals(user); 1323 } 1324 1325 private JCRProjectMember _getProjectMember(Project project, GroupIdentity group) 1326 { 1327 JCRProjectMember projectMember = null; 1328 ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project); 1329 1330 for (AmetysObject memberNode : membersNode.getChildren()) 1331 { 1332 if (memberNode instanceof JCRProjectMember) 1333 { 1334 JCRProjectMember member = (JCRProjectMember) memberNode; 1335 if (MemberType.GROUP == member.getType() && group.equals(member.getGroup())) 1336 { 1337 projectMember = (JCRProjectMember) memberNode; 1338 } 1339 1340 } 1341 } 1342 return projectMember; 1343 } 1344 1345 private JCRProjectMember _getProjectMember(Project project, UserIdentity user) 1346 { 1347 JCRProjectMember projectMember = null; 1348 ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project); 1349 1350 for (AmetysObject memberNode : membersNode.getChildren()) 1351 { 1352 if (memberNode instanceof JCRProjectMember) 1353 { 1354 JCRProjectMember member = (JCRProjectMember) memberNode; 1355 if (MemberType.USER == member.getType() && user.equals(member.getUser())) 1356 { 1357 projectMember = (JCRProjectMember) memberNode; 1358 } 1359 } 1360 } 1361 return projectMember; 1362 } 1363 1364 private void _removeManager(Project project, boolean isTypeUser, UserIdentity user) 1365 { 1366 if (isTypeUser) 1367 { 1368 UserIdentity[] oldManagers = project.getManagers(); 1369 1370 // Remove the user from the project's managers 1371 UserIdentity[] managers = Arrays.stream(oldManagers) 1372 .filter(manager -> !manager.equals(user)) 1373 .toArray(UserIdentity[]::new); 1374 1375 project.setManagers(managers); 1376 } 1377 } 1378 1379 private void _removeMemberProfiles(Project project, JCRProjectMember projectMember) 1380 { 1381 for (WorkspaceModule module : _projectManager.getModules(project)) 1382 { 1383 ModifiableResourceCollection moduleRootNode = module.getModuleRoot(project, false); 1384 _removeMemberProfiles(projectMember, moduleRootNode); 1385 } 1386 } 1387 1388 /** 1389 * Retrieves the users node of the project 1390 * The users node will be created if necessary 1391 * @param project The project 1392 * @return The users node of the project 1393 */ 1394 protected ModifiableTraversableAmetysObject _getProjectMembersNode(Project project) 1395 { 1396 if (project == null) 1397 { 1398 throw new AmetysRepositoryException("Error getting the project users node, project is null"); 1399 } 1400 1401 try 1402 { 1403 ModifiableTraversableAmetysObject membersNode; 1404 if (project.hasChild(__PROJECT_MEMBERS_NODE)) 1405 { 1406 membersNode = project.getChild(__PROJECT_MEMBERS_NODE); 1407 } 1408 else 1409 { 1410 membersNode = project.createChild(__PROJECT_MEMBERS_NODE, __PROJECT_MEMBERS_NODE_TYPE); 1411 } 1412 1413 return membersNode; 1414 } 1415 catch (AmetysRepositoryException e) 1416 { 1417 throw new AmetysRepositoryException("Error getting the project users node", e); 1418 } 1419 } 1420 1421 /** 1422 * Get the JSON representation of a group 1423 * @param group The group 1424 * @return The group 1425 */ 1426 protected Map<String, Object> group2Json(Group group) 1427 { 1428 Map<String, Object> infos = new HashMap<>(); 1429 infos.put("id", GroupIdentity.groupIdentityToString(group.getIdentity())); 1430 infos.put("groupId", group.getIdentity().getId()); 1431 infos.put("label", group.getLabel()); 1432 infos.put("sortablename", group.getLabel()); 1433 infos.put("groupDirectory", group.getIdentity().getDirectoryId()); 1434 return infos; 1435 } 1436 1437 /** 1438 * Count the total of unique users in the project and in the project's group 1439 * @param project The project 1440 * @return The total of members 1441 */ 1442 public Long getMembersCount(Project project) 1443 { 1444 Collection<ProjectMember> projectMembers = getProjectMembers(project, true, false); 1445 1446 return (long) projectMembers.size(); 1447 } 1448 1449 /** 1450 * Get the users from a group that are part of the project. They can be filtered with a predicate 1451 * @param group The group 1452 * @param project The project 1453 * @param filteringPredicate The predicate to filter 1454 * @return The list of users 1455 */ 1456 public List<User> getGroupUsersFromProject(Group group, Project project, BiPredicate<Project, UserIdentity> filteringPredicate) 1457 { 1458 Set<String> projectPopulations = project.getSites() 1459 .stream() 1460 .map(Site::getName) 1461 .map(siteName -> _populationContextHelper.getUserPopulationsOnContexts(List.of("/sites/" + siteName), false, false)) 1462 .flatMap(Set::stream) 1463 .collect(Collectors.toSet()); 1464 1465 return group.getUsers().stream() 1466 .filter(user -> projectPopulations.contains(user.getPopulationId())) 1467 .filter(user -> filteringPredicate.test(project, user)) 1468 .map(_userManager::getUser) 1469 .filter(Objects::nonNull) 1470 .collect(Collectors.toList()); 1471 } 1472 1473 private Cache<ProjectMemberCacheKey, Set<ProjectMember>> _getCache() 1474 { 1475 return _abstractCacheManager.get(__PROJECT_MEMBER_CACHE); 1476 } 1477 1478 /** 1479 * This class represents a member of a project. Could be a user or a group 1480 * 1481 */ 1482 public static class ProjectMember 1483 { 1484 private String _title; 1485 private String _sortableTitle; 1486 private MemberType _type; 1487 private String _role; 1488 private User _user; 1489 private Group _group; 1490 private boolean _isManager; 1491 1492 /** 1493 * Create a project member as a group 1494 * @param title the member's title (user's full name or group's label) 1495 * @param sortableTitle the sortable title 1496 * @param group the group attached to this member. Cannot be null. 1497 */ 1498 public ProjectMember(String title, String sortableTitle, Group group) 1499 { 1500 _title = title; 1501 _sortableTitle = sortableTitle; 1502 _type = MemberType.GROUP; 1503 _role = null; 1504 _isManager = false; 1505 _user = null; 1506 _group = group; 1507 } 1508 1509 /** 1510 * Create a project member as a group 1511 * @param title the member's title (user's full name or group's label) 1512 * @param sortableTitle the sortable title 1513 * @param role the role 1514 * @param isManager true if the member is a manager of the project 1515 * @param user the user attached to this member. Cannot be null. 1516 */ 1517 public ProjectMember(String title, String sortableTitle, User user, String role, boolean isManager) 1518 { 1519 _title = title; 1520 _sortableTitle = sortableTitle; 1521 _type = MemberType.USER; 1522 _role = role; 1523 _isManager = isManager; 1524 _user = user; 1525 _group = null; 1526 } 1527 1528 /** 1529 * Get the title of the member. 1530 * @return The title of the member 1531 */ 1532 public String getTitle() 1533 { 1534 return _title; 1535 } 1536 1537 /** 1538 * Get the sortable title of the member. 1539 * @return The sortable title of the member 1540 */ 1541 public String getSortableTitle() 1542 { 1543 return _sortableTitle; 1544 } 1545 1546 /** 1547 * Get the type of the member. It can be a user or a group 1548 * @return The type of the member 1549 */ 1550 public MemberType getType() 1551 { 1552 return _type; 1553 } 1554 1555 /** 1556 * Get the role of the member. 1557 * @return The role of the member 1558 */ 1559 public String getRole() 1560 { 1561 return _role; 1562 } 1563 1564 /** 1565 * Test if the member is a manager of the project 1566 * @return True if this user is a manager of the project 1567 */ 1568 public boolean isManager() 1569 { 1570 return _isManager; 1571 } 1572 1573 /** 1574 * Get the user of the member. 1575 * @return The user of the member 1576 */ 1577 public User getUser() 1578 { 1579 return _user; 1580 } 1581 1582 /** 1583 * Get the group of the member. 1584 * @return The group of the member 1585 */ 1586 public Group getGroup() 1587 { 1588 return _group; 1589 } 1590 1591 @Override 1592 public boolean equals(Object obj) 1593 { 1594 if (obj == null || !(obj instanceof ProjectMember)) 1595 { 1596 return false; 1597 } 1598 1599 ProjectMember otherMember = (ProjectMember) obj; 1600 1601 if (getType() != otherMember.getType()) 1602 { 1603 return false; 1604 } 1605 1606 if (getType() == MemberType.USER) 1607 { 1608 return getUser().equals(otherMember.getUser()); 1609 } 1610 else 1611 { 1612 return getGroup().equals(otherMember.getGroup()); 1613 } 1614 } 1615 1616 @Override 1617 public int hashCode() 1618 { 1619 return getType() == MemberType.USER ? getUser().getIdentity().hashCode() : getGroup().getIdentity().hashCode(); 1620 } 1621 } 1622 1623 private static final class ProjectMemberCacheKey extends AbstractCacheKey 1624 { 1625 public ProjectMemberCacheKey(String projectId, Boolean extendGroup) 1626 { 1627 super(projectId, extendGroup); 1628 } 1629 1630 public static ProjectMemberCacheKey of(String projectId, Boolean withExpandedGroup) 1631 { 1632 return new ProjectMemberCacheKey(projectId, withExpandedGroup); 1633 } 1634 } 1635}