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