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