001/* 002 * Copyright 2017 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.core.impl.group.directory.ldap; 017 018import java.text.Normalizer; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.Comparator; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.NoSuchElementException; 030import java.util.Objects; 031import java.util.Set; 032import java.util.TreeSet; 033import java.util.function.Function; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036import java.util.stream.Collectors; 037 038import javax.naming.Context; 039import javax.naming.NamingEnumeration; 040import javax.naming.NamingException; 041import javax.naming.directory.Attribute; 042import javax.naming.directory.Attributes; 043import javax.naming.directory.DirContext; 044import javax.naming.directory.InitialDirContext; 045import javax.naming.directory.SearchControls; 046import javax.naming.directory.SearchResult; 047import javax.naming.ldap.InitialLdapContext; 048import javax.naming.ldap.LdapContext; 049 050import org.apache.avalon.framework.activity.Disposable; 051import org.apache.avalon.framework.service.ServiceException; 052import org.apache.avalon.framework.service.ServiceManager; 053import org.apache.commons.lang3.StringUtils; 054import org.slf4j.Logger; 055 056import org.ametys.core.cache.AbstractCacheManager; 057import org.ametys.core.cache.Cache; 058import org.ametys.core.group.Group; 059import org.ametys.core.group.GroupIdentity; 060import org.ametys.core.group.directory.GroupDirectory; 061import org.ametys.core.group.directory.GroupDirectoryModel; 062import org.ametys.core.user.UserIdentity; 063import org.ametys.core.user.UserManager; 064import org.ametys.core.user.directory.UserDirectory; 065import org.ametys.core.user.population.UserPopulationDAO; 066import org.ametys.core.util.Cacheable; 067import org.ametys.core.util.SizeUtils.ExcludeFromSizeCalculation; 068import org.ametys.core.util.ldap.AbstractLDAPConnector; 069import org.ametys.core.util.ldap.ScopeEnumerator; 070import org.ametys.plugins.core.impl.user.LdapUserIdentity; 071import org.ametys.plugins.core.impl.user.directory.LdapUserDirectory; 072import org.ametys.runtime.i18n.I18nizableText; 073import org.ametys.runtime.i18n.I18nizableTextParameter; 074 075/** 076 * Use a LDAP server for getting the groups of users 077 */ 078public class LdapGroupDirectory extends AbstractLDAPConnector implements GroupDirectory, Cacheable, Disposable 079{ 080 /** Name of the parameter holding the datasource id */ 081 protected static final String __PARAM_DATASOURCE_ID = "runtime.groups.ldap.datasource"; 082 /** Name of the parameter holding the id of the associated user directory */ 083 protected static final String __PARAM_ASSOCIATED_USERDIRECTORY_ID = "runtime.groups.ldap.userdirectory"; 084 /** Relative DN for groups. */ 085 protected static final String __PARAM_GROUPS_RELATIVE_DN = "runtime.groups.ldap.groupDN"; 086 /** Filter for limiting the search. */ 087 protected static final String __PARAM_GROUPS_OBJECT_FILTER = "runtime.groups.ldap.filter"; 088 /** The scope used for search. */ 089 protected static final String __PARAM_GROUPS_SEARCH_SCOPE = "runtime.groups.ldap.scope"; 090 /** Name of the id attribute. */ 091 protected static final String __PARAM_GROUPS_ID_ATTRIBUTE = "runtime.groups.ldap.id"; 092 /** Name of the decription attribute. */ 093 protected static final String __PARAM_GROUPS_DESCRIPTION_ATTRIBUTE = "runtime.groups.ldap.description"; 094 095 /** Name of the user uid attribute. */ 096 protected static final String __PARAM_USERS_UID_ATTRIBUTE = "runtime.users.ldap.uidAttr"; 097 /** Name of the member DN attribute. */ 098 protected static final String __PARAM_GROUPS_MEMBER_ATTRIBUTE = "runtime.groups.ldap.member"; 099 /** Name of the member DN attribute. */ 100 protected static final String __PARAM_GROUPS_MEMBEROF_ATTRIBUTE = "runtime.groups.ldap.memberof"; 101 102 private static final GroupComparator __GROUP_COMPARATOR = new GroupComparator(); 103 104 private static final String __LDAP_GROUPDIRECTORY_GROUP_BY_ID_CACHE_NAME_PREFIX = LdapGroupDirectory.class.getName() + "$group.by.id$"; 105 private static final String __LDAP_GROUPDIRECTORY_GROUPS_BY_USER_CACHE_NAME_PREFIX = LdapGroupDirectory.class.getName() + "$groups.by.user$"; 106 private static final String __LDAP_GROUPDIRECTORY_USERS_BY_GROUP_CACHE_NAME_PREFIX = LdapGroupDirectory.class.getName() + "$users.by.group$"; 107 108 109 /** Unique instance of group to mean a null result in the cache */ 110 private static final Group NULLGROUP = new Group() 111 { 112 public GroupIdentity getIdentity() 113 { 114 return null; 115 } 116 117 public String getLabel() 118 { 119 return null; 120 } 121 122 public GroupDirectory getGroupDirectory() 123 { 124 return null; 125 } 126 127 public Set<UserIdentity> getUsers() 128 { 129 return null; 130 } 131 }; 132 133 /** The user manager */ 134 protected UserManager _userManager; 135 /** The DAO for user populations */ 136 protected UserPopulationDAO _userPopulationDAO; 137 138 /** The group DN relative to baseDN */ 139 protected String _groupsRelativeDN; 140 /** The filter to find groups */ 141 protected String _groupsObjectFilter; 142 /** The scope used for search. */ 143 protected int _groupsSearchScope; 144 /** The group id attribute */ 145 protected String _groupsIdAttribute; 146 /** The group description attribute */ 147 protected String _groupsDescriptionAttribute; 148 /** The LDAP search page size. */ 149 protected int _pageSize; 150 151 /** The attribute which contains the member DN */ 152 protected String _groupsMemberAttribute; 153 /** The id of the associated user directory where the LDAP group will retrieve the users */ 154 protected String _associatedUserDirectoryId; 155 /** The id of the associated user population where the LDAP group will retrieve the users */ 156 protected String _associatedPopulationId; 157 /** The user id in 'memberUid' attribute (on groups for retrieving the users of a group). */ 158 protected String _userUidAttribute; 159 160 /** The attribute which contains the groups of a user */ 161 protected String _usersMemberOfAttribute; 162 163 /** The id */ 164 protected String _id; 165 /** The label */ 166 protected I18nizableText _label; 167 /** The id of the {@link GroupDirectoryModel} */ 168 private String _groupDirectoryModelId; 169 /** The map of the values of the parameters */ 170 private Map<String, Object> _paramValues; 171 172 private Pattern _groupExtractionPattern; 173 174 // Cannot use _id as two GroupDirectories with same id can co-exist during a short amount of time (during GroupDirectoryDAO#_read) 175 private final String _uniqueCacheSuffix = org.ametys.core.util.StringUtils.generateKey(); 176 177 private AbstractCacheManager _cacheManager; 178 179 @Override 180 public String getId() 181 { 182 return _id; 183 } 184 185 @Override 186 public I18nizableText getLabel() 187 { 188 return _label; 189 } 190 191 @Override 192 public void setId(String id) 193 { 194 _id = id; 195 } 196 197 @Override 198 public void setLabel(I18nizableText label) 199 { 200 _label = label; 201 } 202 203 @Override 204 public String getGroupDirectoryModelId() 205 { 206 return _groupDirectoryModelId; 207 } 208 209 @Override 210 public Map<String, Object> getParameterValues() 211 { 212 return _paramValues; 213 } 214 215 @Override 216 public void service(ServiceManager serviceManager) throws ServiceException 217 { 218 super.service(serviceManager); 219 _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE); 220 _userPopulationDAO = (UserPopulationDAO) serviceManager.lookup(UserPopulationDAO.ROLE); 221 _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE); 222 } 223 224 @Override 225 public void dispose() 226 { 227 removeCaches(); 228 } 229 230 public AbstractCacheManager getCacheManager() 231 { 232 return _cacheManager; 233 } 234 235 @Override 236 public Collection<SingleCacheConfiguration> getManagedCaches() 237 { 238 return Arrays.asList( 239 SingleCacheConfiguration.of( 240 __LDAP_GROUPDIRECTORY_GROUP_BY_ID_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix(), 241 _buildI18n("PLUGINS_CORE_GROUPS_LDAPGROUP_CACHE_GROUP_BY_ID_LABEL"), 242 _buildI18n("PLUGINS_CORE_GROUPS_LDAPGROUP_CACHE_GROUP_BY_ID_DESC")), 243 SingleCacheConfiguration.of( 244 __LDAP_GROUPDIRECTORY_GROUPS_BY_USER_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix(), 245 _buildI18n("PLUGINS_CORE_GROUPS_LDAPGROUP_CACHE_GROUPS_BY_USER_LABEL"), 246 _buildI18n("PLUGINS_CORE_GROUPS_LDAPGROUP_CACHE_GROUPS_BY_USER_DESC")), 247 SingleCacheConfiguration.of( 248 __LDAP_GROUPDIRECTORY_USERS_BY_GROUP_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix(), 249 _buildI18n("PLUGINS_CORE_GROUPS_LDAPGROUP_CACHE_USERS_BY_GROUP_LABEL"), 250 _buildI18n("PLUGINS_CORE_GROUPS_LDAPGROUP_CACHE_USERS_BY_GROUP_DESC")) 251 ); 252 } 253 254 private I18nizableText _buildI18n(String i18nKey) 255 { 256 String catalogue = "plugin.core-impl"; 257 I18nizableText groupDirectoryId = new I18nizableText(getId()); 258 Map<String, I18nizableTextParameter> labelParams = Map.of("id", groupDirectoryId); 259 return new I18nizableText(catalogue, i18nKey, labelParams); 260 } 261 262 private Cache<String, Group> getCacheGroupById() 263 { 264 return getCacheManager().get(__LDAP_GROUPDIRECTORY_GROUP_BY_ID_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix()); 265 } 266 267 private Cache<UserIdentity, Set<String>> getCacheGroupsByUser() 268 { 269 return getCacheManager().get(__LDAP_GROUPDIRECTORY_GROUPS_BY_USER_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix()); 270 } 271 272 private Cache<GroupIdentity, Set<UserIdentity>> getCacheUsersByGroup() 273 { 274 return getCacheManager().get(__LDAP_GROUPDIRECTORY_USERS_BY_GROUP_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix()); 275 } 276 277 278 private String getUniqueCacheIdSuffix() 279 { 280 return _uniqueCacheSuffix; 281 } 282 283 @Override 284 public void init(String groupDirectoryModelId, Map<String, Object> paramValues) throws Exception 285 { 286 _groupDirectoryModelId = groupDirectoryModelId; 287 _paramValues = paramValues; 288 289 String populationAndUserDirectory = (String) paramValues.get(__PARAM_ASSOCIATED_USERDIRECTORY_ID); 290 String[] split = populationAndUserDirectory.split("#"); 291 _associatedPopulationId = split[0]; 292 _associatedUserDirectoryId = split[1]; 293 294 // FIXME https://issues.ametys.org/browse/RUNTIME-2392 (avoid this check to prevent circular dependency) 295// UserDirectory associatedUserDirectory = _userPopulationDAO.getUserPopulation(_associatedPopulationId).getUserDirectory(_associatedUserDirectoryId); 296// if (!(associatedUserDirectory instanceof LdapUserDirectory)) 297// { 298// throw new IllegalArgumentException("The parameter '" + __PARAM_ASSOCIATED_USERDIRECTORY_ID + "' must reference a LDAP user directory"); 299// } 300 301 _groupsRelativeDN = (String) paramValues.get(__PARAM_GROUPS_RELATIVE_DN); 302 _groupsObjectFilter = (String) paramValues.get(__PARAM_GROUPS_OBJECT_FILTER); 303 _groupsSearchScope = ScopeEnumerator.parseScope((String) paramValues.get(__PARAM_GROUPS_SEARCH_SCOPE)); 304 _groupsIdAttribute = (String) paramValues.get(__PARAM_GROUPS_ID_ATTRIBUTE); 305 _groupsDescriptionAttribute = (String) paramValues.get(__PARAM_GROUPS_DESCRIPTION_ATTRIBUTE); 306 307 _userUidAttribute = (String) paramValues.get(__PARAM_USERS_UID_ATTRIBUTE); 308 309 _groupsMemberAttribute = (String) paramValues.get(__PARAM_GROUPS_MEMBER_ATTRIBUTE); 310 311 _usersMemberOfAttribute = (String) paramValues.get(__PARAM_GROUPS_MEMBEROF_ATTRIBUTE); 312 313 String dataSourceId = (String) paramValues.get(__PARAM_DATASOURCE_ID); 314 try 315 { 316 _delayedInitialize(dataSourceId); 317 } 318 catch (Exception e) 319 { 320 getLogger().error("An error occured during the initialization of LDAPUserDirectory", e); 321 } 322 323 _pageSize = __DEFAULT_PAGE_SIZE; 324 325 _groupExtractionPattern = Pattern.compile("^" + _groupsIdAttribute + "=([^,]+),.*", Pattern.CASE_INSENSITIVE); 326 327 createCaches(); 328 } 329 330 @Override 331 public Group getGroup(String groupID) 332 { 333 Group group = null; 334 335 // Cache hit, return the results. 336 if (isCachingEnabled() && getCacheGroupById().hasKey(groupID)) 337 { 338 Group groupCache = getCacheGroupById().get(groupID); 339 if (NULLGROUP == groupCache) 340 { 341 return null; 342 } 343 else 344 { 345 return groupCache; 346 } 347 } 348 349 DirContext context = null; 350 NamingEnumeration results = null; 351 352 try 353 { 354 // Connect to ldap server 355 context = new InitialDirContext(_getContextEnv()); 356 357 // Create search filter 358 StringBuffer filter = new StringBuffer("(&"); 359 filter.append(_groupsObjectFilter); 360 filter.append("("); 361 filter.append(_groupsIdAttribute); 362 filter.append("={0}))"); 363 364 // Run search 365 results = context.search(_groupsRelativeDN, filter.toString(), 366 new Object[] {groupID}, _getSearchConstraint()); 367 368 // Check if a group matches 369 if (results.hasMoreElements()) 370 { 371 // Retrieve the found group 372 group = _getUserGroup((SearchResult) results.nextElement()); 373 } 374 } 375 catch (IllegalArgumentException e) 376 { 377 getLogger().error("Error missing at least one attribute or attribute value", e); 378 } 379 catch (NamingException e) 380 { 381 getLogger().error("Error communication with ldap server", e); 382 } 383 finally 384 { 385 // Close connection resources 386 _cleanup(context, results); 387 } 388 389 // Cache the results. 390 if (isCachingEnabled()) 391 { 392 getCacheGroupById().put(groupID, group != null ? group : NULLGROUP); 393 } 394 395 // Return group or null 396 return group; 397 } 398 399 @Override 400 public Set<Group> getGroups() 401 { 402 Set<Group> groups = new TreeSet<>(__GROUP_COMPARATOR); 403 404 groups.addAll(getGroups(-1, 0, Collections.emptyMap())); 405 406 // Return the list of users as a collection of UserGroup, possibly empty 407 return groups; 408 } 409 410 @Override 411 public Set<String> getUserGroups(UserIdentity userIdentity) 412 { 413 String populationId = userIdentity.getPopulationId(); 414 415 if (!populationId.equals(_associatedPopulationId)) 416 { 417 return Collections.emptySet(); 418 } 419 420 // Cache hit, return the results. 421 if (isCachingEnabled() && getCacheGroupsByUser().hasKey(userIdentity)) 422 { 423 Set<String> userGroups = getCacheGroupsByUser().get(userIdentity); 424 return userGroups; 425 } 426 427 Set<String> groups; 428 429 UserDirectory associatedUserDirectory = _userPopulationDAO.getUserPopulation(_associatedPopulationId).getUserDirectory(_associatedUserDirectoryId); 430 431 if (!(associatedUserDirectory instanceof LdapUserDirectory)) 432 { 433 getLogger().warn("A Ldap group directory must be associated with a Ldap user directory."); 434 return Set.of(); 435 } 436 437 LdapUserDirectory associatedLdapUserDirectory = (LdapUserDirectory) associatedUserDirectory; 438 439 String usersRelativeDN = (String) associatedLdapUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_RELATIVE_DN); 440 441 // If param 'runtime.groups.ldap.memberof' is present, try to read the property from the user entries 442 if (StringUtils.isNotEmpty(_usersMemberOfAttribute)) 443 { 444 groups = _getUserGroupsFromMemberofAttr(userIdentity, usersRelativeDN, associatedLdapUserDirectory); 445 } 446 // If param 'runtime.groups.ldap.memberof' is not present, 'runtime.groups.ldap.member' must be present, try to read the property from the group entries 447 else 448 { 449 groups = _getUserGroupsFromMemberAttr(userIdentity, associatedLdapUserDirectory); 450 } 451 452 // Cache the results. 453 if (isCachingEnabled()) 454 { 455 getCacheGroupsByUser().put(userIdentity, groups); 456 } 457 458 // Return the groups, possibly empty 459 return groups; 460 } 461 462 private Set<String> _getUserGroupsFromMemberofAttr(UserIdentity userIdentity, String usersRelativeDN, LdapUserDirectory associatedUserDirectory) 463 { 464 Set<String> groups = new HashSet<>(); 465 466 String login = userIdentity.getLogin(); 467 DirContext context = null; 468 NamingEnumeration userResults = null; 469 470 try 471 { 472 // Connect to ldap server 473 context = new InitialDirContext(_getContextEnv()); 474 Attributes userAttrs = null; 475 476 if (userIdentity instanceof LdapUserIdentity) 477 { 478 // Lookup the user and get the attribute of the groups 479 String dn = ((LdapUserIdentity) userIdentity).getDn(); 480 String relativeDn = _getRelativeDn(dn); 481 482 userAttrs = context.getAttributes(relativeDn, new String[] {_usersMemberOfAttribute}); 483 } 484 else 485 { 486 // Search user with given login attribute 487 String userLoginAttribute = (String) associatedUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_LOGIN_ATTRIBUTE); 488 String usersObjectFilter = (String) associatedUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_OBJECT_FILTER); 489 490 // Create search filter 491 StringBuffer userFilter = new StringBuffer("(&"); 492 userFilter.append(usersObjectFilter); 493 userFilter.append("("); 494 userFilter.append(userLoginAttribute); 495 userFilter.append("={0}))"); 496 497 getLogger().debug("Searching groups of user '{}' on user itself: '{}'.", login, userFilter); 498 499 // Run search 500 userResults = context.search(usersRelativeDN, userFilter.toString(), new Object[] {login}, _getUserSearchConstraint(new String[] {userLoginAttribute, _usersMemberOfAttribute})); 501 502 // Fill the set of groups 503 if (userResults.hasMoreElements()) 504 { 505 // The search result should not send more than one result as it is an id 506 SearchResult userResult = (SearchResult) userResults.nextElement(); 507 userAttrs = userResult.getAttributes(); 508 } 509 userResults.close(); 510 } 511 512 // The user may come from the good population (the associated population), but from a user diretory which is not the LDAP one 513 if (userAttrs != null) 514 { 515 groups.addAll(_getGroupIdsOfUser(userAttrs, context)); 516 } 517 } 518 catch (NamingException e) 519 { 520 getLogger().error("Error communication with ldap server", e); 521 } 522 finally 523 { 524 // Close connection resources 525 _cleanup(context, userResults); 526 } 527 528 getLogger().debug("{} groups found for user '{}' from '{}' attribute on users", groups.size(), login, _usersMemberOfAttribute); 529 530 return groups; 531 } 532 533 private Set<String> _getUserGroupsFromMemberAttr(UserIdentity userIdentity, LdapUserDirectory userDirectory) 534 { 535 Set<String> groups = new HashSet<>(); 536 String login = userIdentity.getLogin(); 537 538 DirContext context = null; 539 NamingEnumeration userResults = null; 540 541 // Create search filter 542 StringBuffer groupFilter = new StringBuffer("(&"); 543 groupFilter.append(_groupsObjectFilter); 544 545 groupFilter.append("(|"); 546 547 String dn = userDirectory.getUserDN(login); 548 549 // If 'runtime.groups.ldap.member' references a DN 550 groupFilter.append("("); 551 groupFilter.append(_groupsMemberAttribute); 552 groupFilter.append("={0}"); 553 groupFilter.append(")"); 554 555 // If 'runtime.groups.ldap.member' references a UID 556 groupFilter.append("("); 557 groupFilter.append(_groupsMemberAttribute); 558 groupFilter.append("={1})"); 559 560 groupFilter.append("))"); 561 562 getLogger().debug("Searching groups of user '{}' with base DN '{}': '{}'.", login, _groupsRelativeDN, groupFilter); 563 564 // Run search 565 int groupCount = 0; 566 try 567 { 568 // Connect to ldap server 569 context = new InitialDirContext(_getContextEnv()); 570 571 userResults = context.search(_groupsRelativeDN, groupFilter.toString(), 572 new Object[] {dn, login}, _getSearchConstraint()); 573 574 // Fill the set of groups 575 while (userResults.hasMoreElements()) 576 { 577 // Retrieve the found group 578 String groupId = _getGroupId((SearchResult) userResults.nextElement()); 579 if (groupId != null) 580 { 581 groups.add(groupId); 582 groupCount++; 583 } 584 } 585 } 586 catch (NamingException e) 587 { 588 getLogger().error("Error communication with ldap server", e); 589 } 590 finally 591 { 592 // Close connection resources 593 _cleanup(context, userResults); 594 } 595 596 getLogger().debug("{} groups found for user '{}' from '{}' attribute on groups", groupCount, login, _groupsMemberAttribute); 597 598 return groups; 599 } 600 601 /** 602 * Get a group id from attributes of a ldap group entry. 603 * @param groupEntry The ldap group entry to get attributes from. 604 * @return The group id as a String. 605 * @throws IllegalArgumentException If a needed attribute is missing. 606 */ 607 protected String _getGroupId(SearchResult groupEntry) 608 { 609 // Retrieve the attributes of the entry 610 Attributes attrs = groupEntry.getAttributes(); 611 612 try 613 { 614 // Retrieve the identifier of a group 615 Attribute groupIDAttr = attrs.get(_groupsIdAttribute); 616 if (groupIDAttr == null) 617 { 618 getLogger().warn("Missing group id attribute : \"{}\". Group will be ignored.", _groupsIdAttribute); 619 return null; 620 } 621 622 return (String) groupIDAttr.get(); 623 } 624 catch (NamingException e) 625 { 626 getLogger().warn("Missing at least one value for an attribute in an ldap entry. Group will be ignored.", e); 627 return null; 628 } 629 } 630 631 /** 632 * Get group ids from attributes of a ldap user entry. 633 * @param userAttrs The attributes of a ldap user entry 634 * @param context The context 635 * @return The group ids as a Set of String. 636 * @throws NamingException If a naming exception was encountered while retrieving the group DNs 637 * @throws IllegalArgumentException If a needed attribute is missing. 638 */ 639 @SuppressWarnings("unchecked") 640 protected Set<String> _getGroupIdsOfUser(Attributes userAttrs, DirContext context) throws NamingException 641 { 642 Set<String> groups = new HashSet<>(); 643 644 // Retrieve the identifier of the groups 645 Attribute userGroups = userAttrs.get(_usersMemberOfAttribute); 646 if (userGroups != null) 647 { 648 NamingEnumeration<String> groupIds = null; 649 try 650 { 651 // Retrieve the members of the group 652 groupIds = (NamingEnumeration<String>) userGroups.getAll(); 653 while (groupIds.hasMoreElements()) 654 { 655 String groupId = groupIds.nextElement(); 656 657 Matcher matcher = _groupExtractionPattern.matcher(groupId); 658 if (matcher.matches()) 659 { 660 // the group id is a DN, we can look it up in the directory 661 try 662 { 663 String relativeGroupDn = _getRelativeDn(groupId); 664 Attributes groupAttrs = context.getAttributes(relativeGroupDn, new String[] {_groupsIdAttribute}); 665 Attribute groupIdAttr = groupAttrs.get(_groupsIdAttribute); 666 if (groupIdAttr != null) 667 { 668 groups.add((String) groupIdAttr.get()); 669 } 670 } 671 catch (NamingException e) 672 { 673 getLogger().warn(String.format("Unable to get the group from the LDAP DN entry: %s", groupId), e); 674 } 675 } 676 else 677 { 678 // the group is only an id 679 // FIXME we could try to search for this id in case it does does actually represent a group, but it has a performance cost 680 groups.add(groupId); 681 } 682 } 683 } 684 finally 685 { 686 _cleanup(null, groupIds); 687 } 688 } 689 690 return groups; 691 } 692 693 @Override 694 public List<Group> getGroups(int count, int offset, Map parameters) 695 { 696 String pattern = (String) parameters.get("pattern"); 697 698 try 699 { 700 List<SearchResult> searchResults = _search(_pageSize, _groupsRelativeDN, _groupsObjectFilter, _getSearchConstraint(), count > -1 && _serverSideSorting); 701 702 return searchResults.stream() 703 .map(this::_getUserGroup) 704 .filter(Objects::nonNull) 705 .filter(group -> _filterMatchingGroup(group, pattern)) 706 .skip(offset) 707 .limit(count < 0 ? Integer.MAX_VALUE : count) 708 .collect(Collectors.toList()); 709 } 710 catch (NamingException e) 711 { 712 getLogger().error("Error of communication with ldap server", e); 713 } 714 715 return Collections.emptyList(); 716 } 717 718 private boolean _filterMatchingGroup(Group group, String pattern) 719 { 720 if (StringUtils.isEmpty(pattern)) 721 { 722 return true; 723 } 724 Function<String, String> normalize = str -> Normalizer.normalize(str.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim(); 725 String toMatch = normalize.apply(pattern); 726 if (StringUtils.isEmpty(toMatch)) 727 { 728 return true; 729 } 730 731 String groupLabel = normalize.apply(group.getLabel()); 732 if (groupLabel.indexOf(toMatch) != -1) 733 { 734 return true; 735 } 736 737 String groupIdentity = group.getIdentity() != null ? normalize.apply(group.getIdentity().getId()) : null; 738 return groupIdentity != null && groupIdentity.indexOf(toMatch) != -1; 739 } 740 741 /** 742 * Get an UserGroup from attributes of a ldap entry. 743 * @param entry The ldap entry to get attributes from. 744 * @return The group as an UserGroup. 745 * @throws IllegalArgumentException If a needed attribute is missing. 746 */ 747 protected Group _getUserGroup(SearchResult entry) 748 { 749 LdapGroup group = null; 750 // Retrieve the attributes of the entry 751 Attributes attrs = entry.getAttributes(); 752 753 try 754 { 755 // Retrieve the identifier of a group 756 Attribute groupIDAttr = attrs.get(_groupsIdAttribute); 757 if (groupIDAttr == null) 758 { 759 getLogger().warn("Missing group id attribute : \"" + _groupsIdAttribute + "\". Group will be ignored."); 760 return null; 761 } 762 String groupID = (String) groupIDAttr.get(); 763 764 // Retrieve the description of a group 765 Attribute groupDESCAttr = attrs.get(_groupsDescriptionAttribute); 766 if (groupDESCAttr == null) 767 { 768 getLogger().warn("Missing group description attribute : \"" + _groupsDescriptionAttribute + "\". Group will be ignored."); 769 return null; 770 } 771 String groupDesc = (String) groupDESCAttr.get(); 772 773 Attribute membersAttr = null; 774 if (StringUtils.isNotEmpty(_groupsMemberAttribute)) 775 { 776 membersAttr = attrs.get(_groupsMemberAttribute); 777 } 778 group = new LdapGroup(new GroupIdentity(groupID, getId()), groupDesc, this, membersAttr, getLogger()); 779 } 780 catch (NamingException e) 781 { 782 getLogger().warn("Missing at least one value for an attribute in an ldap entry. Group will be ignored.", e); 783 return null; 784 } 785 catch (IllegalArgumentException e) 786 { 787 getLogger().error("Error missing at least one attribute or attribute value", e); 788 } 789 790 return group; 791 } 792 793 /** 794 * If the given DN is absolute, return the relative DN. Otherwise, return the given DN. 795 * @param dn The absolute or relative DN 796 * @return The relative DN 797 */ 798 protected String _getRelativeDn(String dn) 799 { 800 String relativeDn = dn; 801 String suffix = "," + _ldapBaseDN; 802 if (dn.endsWith(suffix)) 803 { 804 relativeDn = StringUtils.substring(dn, 0, -suffix.length()); 805 } 806 807 return relativeDn; 808 } 809 810 /** 811 * Gets a user according to its DN 812 * @param ldapDn The DN of the user in the LDAP 813 * @return A user 814 */ 815 protected UserIdentity _getUserInLdapFromDn(String ldapDn) 816 { 817 UserDirectory associatedUserDirectory = _userPopulationDAO.getUserPopulation(_associatedPopulationId).getUserDirectory(_associatedUserDirectoryId); 818 String userLoginAttribute = (String) associatedUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_LOGIN_ATTRIBUTE); 819 820 String relativeDn = _getRelativeDn(ldapDn); 821 822 LdapContext ldapContext = null; 823 try 824 { 825 ldapContext = new InitialLdapContext(_getContextEnv(), null); 826 Attributes userAttrs = ldapContext.getAttributes(relativeDn, new String[] {userLoginAttribute}); 827 Attribute userLogin = userAttrs.get(userLoginAttribute); 828 if (userLogin == null) 829 { 830 getLogger().warn("User '{}' was found in LDAP but is missing the attribute {}", relativeDn, userLoginAttribute); 831 return null; 832 } 833 UserIdentity identity = new UserIdentity((String) userLogin.get(), _associatedPopulationId); 834 if (_userManager.getUser(identity) != null) 835 { 836 return identity; 837 } 838 else 839 { 840 getLogger().warn("User with login '{}' was found in LDAP but is not a user of the population {}", userLogin.get(), _associatedPopulationId); 841 } 842 } 843 catch (NamingException e) 844 { 845 getLogger().warn(String.format("Unable to get the user from the LDAP DN entry: %s", ldapDn), e); 846 } 847 finally 848 { 849 _cleanup(ldapContext, null); 850 } 851 852 return null; 853 } 854 855 /** 856 * Gets a user according to its UID 857 * @param ldapUid The UID of the user in the LDAP 858 * @return A user 859 */ 860 protected UserIdentity _getUserInLdapFromUid(String ldapUid) 861 { 862 UserDirectory associatedUserDirectory = _userPopulationDAO.getUserPopulation(_associatedPopulationId).getUserDirectory(_associatedUserDirectoryId); 863 String userLoginAttribute = (String) associatedUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_LOGIN_ATTRIBUTE); 864 String usersRelativeDN = (String) associatedUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_RELATIVE_DN); 865 866 try 867 { 868 String filter = _userUidAttribute + "=" + ldapUid; 869 SearchControls constraints = _getUserSearchConstraint(new String[] {userLoginAttribute}); 870 871 List<SearchResult> results = _search(_pageSize, usersRelativeDN, filter, constraints, false); 872 if (results.size() > 0) 873 { 874 SearchResult searchResult = results.get(0); 875 Attribute userLogin = searchResult.getAttributes().get(userLoginAttribute); 876 if (userLogin == null) 877 { 878 getLogger().warn("User '{}' was found in LDAP but is missing the attribute {}", searchResult, userLoginAttribute); 879 return null; 880 } 881 UserIdentity identity = new UserIdentity((String) userLogin.get(), _associatedPopulationId); 882 if (_userManager.getUser(identity) != null) 883 { 884 return identity; 885 } 886 else 887 { 888 getLogger().warn("User with login '{}' was found in LDAP but is not a user of the population {}", userLogin.get(), _associatedPopulationId); 889 } 890 } 891 getLogger().warn("Unable to get the user from the LDAP UID: {}", ldapUid); 892 return null; 893 } 894 catch (NamingException | NoSuchElementException e) 895 { 896 getLogger().warn(String.format("Unable to get the user from the LDAP UID: %s", ldapUid), e); 897 return null; 898 } 899 } 900 901 /** 902 * Gets all users of a group from the 'runtime.groups.ldap.memberof' attribute on the users 903 * @param groupId The id of the group 904 * @return The users of the given group, only by looking at the 'runtime.groups.ldap.memberof' attribute on the users 905 */ 906 protected Set<UserIdentity> _getUsersFromMembersOfAttr(String groupId) 907 { 908 Set<UserIdentity> identities = new LinkedHashSet<>(); 909 if (_usersMemberOfAttribute == null) 910 { 911 return identities; 912 } 913 914 UserDirectory associatedUserDirectory = _userPopulationDAO.getUserPopulation(_associatedPopulationId).getUserDirectory(_associatedUserDirectoryId); 915 String userLoginAttribute = (String) associatedUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_LOGIN_ATTRIBUTE); 916 String usersRelativeDN = (String) associatedUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_RELATIVE_DN); 917 String usersObjectFilter = (String) associatedUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_OBJECT_FILTER); 918 919 try 920 { 921 // FIXME work only if the group id attribute in Ametys is the same as the id in the LDAP ? Would be better if a GroupIdentity owned the DN of the group 922 String memberOfValue = _groupsIdAttribute + "=" + groupId + "," + _groupsRelativeDN + "," + _ldapBaseDN; 923 String filter = "(&" + usersObjectFilter + "(|(" + _usersMemberOfAttribute + "=" + groupId + ")(" + _usersMemberOfAttribute + "=" + memberOfValue + ")))"; 924 925 List<SearchResult> searchResults = _search(_pageSize, usersRelativeDN, filter, _getUserSearchConstraint(new String[] {userLoginAttribute}), false); 926 for (SearchResult searchResult : searchResults) 927 { 928 Attributes attrs = searchResult.getAttributes(); 929 Attribute userLogin = attrs.get(userLoginAttribute); 930 if (userLogin == null) 931 { 932 getLogger().warn("User '{}' was found in LDAP but is missing the attribute {}", searchResult, userLoginAttribute); 933 break; 934 } 935 UserIdentity identity = new UserIdentity((String) userLogin.get(), _associatedPopulationId); 936 if (_userManager.getUser(identity) != null) 937 { 938 identities.add(identity); 939 } 940 else 941 { 942 getLogger().warn("User with login '{}' was found in LDAP but is not a user of the population {}", userLogin.get(), _associatedPopulationId); 943 } 944 } 945 } 946 catch (NamingException e) 947 { 948 getLogger().error("Error of communication with ldap server", e); 949 } 950 951 return identities; 952 } 953 954 private SearchControls _getUserSearchConstraint(String[] returningAttributes) 955 { 956 // Search parameters 957 SearchControls constraints = new SearchControls(); 958 959 // Position the wanted attributes 960 constraints.setReturningAttributes(returningAttributes); 961 962 // Choose depth of search 963 UserDirectory associatedUserDirectory = _userPopulationDAO.getUserPopulation(_associatedPopulationId).getUserDirectory(_associatedUserDirectoryId); 964 int usersSearchScope = ScopeEnumerator.parseScope((String) associatedUserDirectory.getParameterValues().get(LdapUserDirectory.PARAM_USERS_SEARCH_SCOPE)); 965 constraints.setSearchScope(usersSearchScope); 966 967 return constraints; 968 } 969 970 /** 971 * Get constraints for a search. 972 * @return The constraints as a SearchControls. 973 */ 974 protected SearchControls _getSearchConstraint() 975 { 976 // Search parameters 977 SearchControls constraints = new SearchControls(); 978 979 // Only one attribute to retrieve 980 constraints.setReturningAttributes(new String [] {_groupsIdAttribute, _groupsDescriptionAttribute, _groupsMemberAttribute}); 981 // Choose the depth of search 982 constraints.setSearchScope(_groupsSearchScope); 983 return constraints; 984 } 985 986 /** 987 * Get group as JSON object 988 * @param group the group 989 * @param users true to get users' group 990 * @return the group as JSON object 991 */ 992 protected Map<String, Object> _group2JSON(Group group, boolean users) 993 { 994 Map<String, Object> group2json = new HashMap<>(); 995 group2json.put("id", group.getIdentity().getId()); 996 group2json.put("groupDirectory", group.getIdentity().getDirectoryId()); 997 group2json.put("groupDirectoryLabel", group.getGroupDirectory().getLabel()); 998 group2json.put("label", group.getLabel()); 999 if (users) 1000 { 1001 group2json.put("users", group.getUsers()); 1002 } 1003 return group2json; 1004 } 1005 1006 /** 1007 * Group comparator. 1008 */ 1009 private static class GroupComparator implements Comparator<Group> 1010 { 1011 /** 1012 * Constructor. 1013 */ 1014 public GroupComparator() 1015 { 1016 // Nothing to do. 1017 } 1018 1019 @Override 1020 public int compare(Group g1, Group g2) 1021 { 1022 if (g1.getIdentity().getId().equals(g2.getIdentity().getId())) 1023 { 1024 return 0; 1025 } 1026 1027 // Case insensitive sort 1028 int compareTo = g1.getLabel().toLowerCase().compareTo(g2.getLabel().toLowerCase()); 1029 if (compareTo == 0) 1030 { 1031 return g1.getIdentity().getId().compareTo(g2.getIdentity().getId()); 1032 } 1033 return compareTo; 1034 } 1035 } 1036 1037 /** 1038 * Implementation of {@link Group} for Ldap group directory 1039 */ 1040 private static final class LdapGroup implements Group 1041 { 1042 private boolean _userInitialized; 1043 private Set<UserIdentity> _users; 1044 private GroupIdentity _identity; 1045 private String _groupLabel; 1046 private Attribute _membersAttr; 1047 1048 @ExcludeFromSizeCalculation 1049 private Logger _logger; 1050 1051 @ExcludeFromSizeCalculation 1052 private LdapGroupDirectory _groupDirectory; 1053 1054 LdapGroup(GroupIdentity identity, String label, LdapGroupDirectory groupDirectory, Attribute membersAttr, Logger logger) 1055 { 1056 _identity = identity; 1057 _groupLabel = label; 1058 _groupDirectory = groupDirectory; 1059 _membersAttr = membersAttr; 1060 _logger = logger; 1061 _userInitialized = false; 1062 _users = new HashSet<>(); 1063 } 1064 1065 @Override 1066 public GroupIdentity getIdentity() 1067 { 1068 return _identity; 1069 } 1070 1071 @Override 1072 public String getLabel() 1073 { 1074 return _groupLabel; 1075 } 1076 1077 @Override 1078 public GroupDirectory getGroupDirectory() 1079 { 1080 return _groupDirectory; 1081 } 1082 1083 @Override 1084 public Set<UserIdentity> getUsers() 1085 { 1086 if (!_userInitialized) 1087 { 1088 if (_hasUsersFromCache(_identity)) 1089 { 1090 _users.addAll(_getUsersFromCache(_identity)); 1091 } 1092 else 1093 { 1094 _users.addAll(_membersAttr != null ? _getUsersFromMembersAttr() 1095 : _groupDirectory._getUsersFromMembersOfAttr(_identity.getId())); 1096 _loadUsersInCache(_identity, _users); 1097 } 1098 _userInitialized = true; 1099 } 1100 return _users; 1101 } 1102 1103 private boolean _hasUsersFromCache(GroupIdentity groupIdentity) 1104 { 1105 return _groupDirectory.isCachingEnabled() 1106 && _groupDirectory.getCacheUsersByGroup().hasKey(groupIdentity); 1107 } 1108 1109 private Set<UserIdentity> _getUsersFromCache(GroupIdentity groupIdentity) // only call if _hasUsersFromCache returned true before 1110 { 1111 _logger.debug("Users found in cache for group '{}", groupIdentity); 1112 return _groupDirectory.getCacheUsersByGroup().get(groupIdentity); 1113 } 1114 1115 private void _loadUsersInCache(GroupIdentity groupIdentity, Set<UserIdentity> users) 1116 { 1117 if (_groupDirectory.isCachingEnabled()) 1118 { 1119 _groupDirectory.getCacheUsersByGroup().put(groupIdentity, users); 1120 _logger.debug("Users loaded in cache for group '{}", groupIdentity); 1121 } 1122 } 1123 1124 private Set<UserIdentity> _getUsersFromMembersAttr() 1125 { 1126 Set<UserIdentity> users = new HashSet<>(); 1127 1128 // First fill an intermediate list of members as strings 1129 // Do not resolve yet in order to close enumeration ASAP 1130 NamingEnumeration members = null; 1131 List<String> userDNs = new ArrayList<>(); 1132 try 1133 { 1134 // Retrieve the members of the group 1135 members = _membersAttr.getAll(); 1136 while (members.hasMore()) 1137 { 1138 String userDN = (String) members.next(); 1139 userDNs.add(userDN); 1140 } 1141 } 1142 catch (NamingException e) 1143 { 1144 _logger.warn("Missing at least one value for an attribute in an ldap entry. Group will be ignored.", e); 1145 } 1146 finally 1147 { 1148 _cleanup(null, members); 1149 } 1150 1151 // Then resolve the users 1152 for (String userDN : userDNs) 1153 { 1154 // Retrieve the identity 1155 UserIdentity identity = _isDn(userDN) ? _groupDirectory._getUserInLdapFromDn(userDN) 1156 : _groupDirectory._getUserInLdapFromUid(userDN); 1157 1158 if (identity != null) 1159 { 1160 // Add the curent user 1161 users.add(identity); 1162 } 1163 } 1164 1165 return users; 1166 } 1167 1168 private boolean _isDn(String userDN) 1169 { 1170 // Let's say that if it contains the '=' character, it is a DN, otherwise it is a UID 1171 return userDN.contains("="); 1172 } 1173 1174 @SuppressWarnings("synthetic-access") 1175 private void _cleanup(Context context, NamingEnumeration members) 1176 { 1177 _groupDirectory._cleanup(context, members); 1178 } 1179 1180 @Override 1181 public String toString() 1182 { 1183 StringBuffer sb = new StringBuffer("UserGroup["); 1184 sb.append(_identity); 1185 sb.append(" ("); 1186 sb.append(_groupLabel); 1187 sb.append(") => "); 1188 if (_userInitialized) 1189 { 1190 sb.append(_users.toString()); 1191 } 1192 else 1193 { 1194 sb.append("\"Users are not loaded yet\""); 1195 } 1196 sb.append("]"); 1197 return sb.toString(); 1198 } 1199 1200 @Override 1201 public boolean equals(Object another) 1202 { 1203 if (another == null || !(another instanceof LdapGroup)) 1204 { 1205 return false; 1206 } 1207 1208 LdapGroup otherGroup = (LdapGroup) another; 1209 1210 return _identity != null && _identity.equals(otherGroup.getIdentity()); 1211 } 1212 1213 @Override 1214 public int hashCode() 1215 { 1216 return _identity.hashCode(); 1217 } 1218 } 1219 1220 @Override 1221 protected String[] getSortByFields() 1222 { 1223 return new String[] {_groupsDescriptionAttribute}; 1224 } 1225}