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