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