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}