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