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