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