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