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