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