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