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