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