001/*
002 *  Copyright 2016 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.user.directory;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collection;
022import java.util.HashMap;
023import java.util.Hashtable;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027
028import javax.naming.AuthenticationException;
029import javax.naming.Name;
030import javax.naming.NameParser;
031import javax.naming.NamingEnumeration;
032import javax.naming.NamingException;
033import javax.naming.PartialResultException;
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.Control;
041import javax.naming.ldap.InitialLdapContext;
042import javax.naming.ldap.LdapContext;
043import javax.naming.ldap.SortControl;
044
045import org.apache.avalon.framework.activity.Disposable;
046import org.apache.avalon.framework.component.Component;
047import org.apache.avalon.framework.service.ServiceException;
048import org.apache.avalon.framework.service.ServiceManager;
049import org.apache.commons.lang3.StringUtils;
050
051import org.ametys.core.cache.AbstractCacheManager;
052import org.ametys.core.cache.Cache;
053import org.ametys.core.user.User;
054import org.ametys.core.user.directory.NotUniqueUserException;
055import org.ametys.core.user.directory.UserDirectory;
056import org.ametys.core.util.Cacheable;
057import org.ametys.core.util.ldap.AbstractLDAPConnector;
058import org.ametys.core.util.ldap.ScopeEnumerator;
059import org.ametys.plugins.core.impl.user.LdapUserIdentity;
060import org.ametys.runtime.i18n.I18nizableTextParameter;
061import org.ametys.runtime.i18n.I18nizableText;
062
063/**
064 * Use an ldap directory for getting the list of users and also authenticating
065 * them.<br>
066 */
067public class LdapUserDirectory extends AbstractLDAPConnector implements UserDirectory, Component, Cacheable, Disposable
068{
069    /** Name of the parameter holding the datasource id */
070    public static final String PARAM_DATASOURCE_ID = "runtime.users.ldap.datasource";
071    /** Relative DN for users. */
072    public static final String PARAM_USERS_RELATIVE_DN = "runtime.users.ldap.peopleDN";
073    /** Filter for limiting the search. */
074    public static final String PARAM_USERS_OBJECT_FILTER = "runtime.users.ldap.baseFilter";
075    /** The scope used for search. */
076    public static final String PARAM_USERS_SEARCH_SCOPE = "runtime.users.ldap.scope";
077    /** Name of the login attribute. */
078    public static final String PARAM_USERS_LOGIN_ATTRIBUTE = "runtime.users.ldap.loginAttr";
079    /** Name of the first name attribute. */
080    public static final String PARAM_USERS_FIRSTNAME_ATTRIBUTE = "runtime.users.ldap.firstnameAttr";
081    /** Name of the last name attribute. */
082    public static final String PARAM_USERS_LASTNAME_ATTRIBUTE = "runtime.users.ldap.lastnameAttr";
083    /** Name of the email attribute. */
084    public static final String PARAM_USERS_EMAIL_ATTRIBUTE = "runtime.users.ldap.emailAttr";
085    /** To know if email is a mandatory attribute */
086    public static final String PARAM_USERS_EMAIL_IS_MANDATORY = "runtime.users.ldap.emailMandatory";
087    /** True to sort the results on the server side, false to get the results unsorted. */
088    public static final String PARAM_SERVER_SIDE_SORTING = "runtime.users.ldap.serverSideSorting";
089    
090    private static final String __LDAP_USERDIRECTORY_USER_BY_LOGIN_CACHE_NAME_PREFIX = LdapUserDirectory.class.getName() + "$by.login$";
091    private static final String __LDAP_USERDIRECTORY_USER_BY_MAIL_CACHE_NAME_PREFIX = LdapUserDirectory.class.getName() + "$by.mail$";
092
093    
094    /** Relative DN for users. */
095    protected String _usersRelativeDN;
096    /** Filter for limiting the search. */
097    protected String _usersObjectFilter;
098    /** The scope used for search. */
099    protected int _usersSearchScope;
100    /** Name of the login attribute. */
101    protected String _usersLoginAttribute;
102    /** Name of the first name attribute. */
103    protected String _usersFirstnameAttribute;
104    /** Name of the last name attribute. */
105    protected String _usersLastnameAttribute;
106    /** Name of the email attribute. */
107    protected String _usersEmailAttribute;
108    /** To know if email is a mandatory attribute */
109    protected boolean _userEmailIsMandatory;
110    /** The LDAP search page size. */
111    protected int _pageSize;
112    
113    private String _udModelId;
114    private Map<String, Object> _paramValues;
115    private String _populationId;
116    private String _label;
117    private String _id;
118    
119    // Cannot use _populationId + "#" + _id as two UserDirectories with same id can co-exist during a short amount of time (during UserPopulationDAO#_readPopulations)
120    private final String _uniqueCacheSuffix = org.ametys.core.util.StringUtils.generateKey();
121    
122    private AbstractCacheManager _cacheManager;
123    
124    public String getId()
125    {
126        return _id;
127    }
128    
129    public String getLabel()
130    {
131        return _label;
132    }
133    
134    @Override
135    public void service(ServiceManager serviceManager) throws ServiceException
136    {
137        super.service(serviceManager);
138        _cacheManager = (AbstractCacheManager) serviceManager.lookup(AbstractCacheManager.ROLE);
139    }
140    
141    @Override
142    public void dispose()
143    {
144        removeCaches();
145    }
146
147    @Override
148    public AbstractCacheManager getCacheManager()
149    {
150        return _cacheManager;
151    }
152    
153    private String getUniqueCacheIdSuffix()
154    {
155        return _uniqueCacheSuffix;
156    }
157    
158    @Override
159    public Collection<SingleCacheConfiguration> getManagedCaches()
160    {
161        return Arrays.asList(
162                SingleCacheConfiguration.of(
163                        __LDAP_USERDIRECTORY_USER_BY_LOGIN_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix(), 
164                        _buildI18n("PLUGINS_CORE_USERS_LDAPUSER_CACHE_BY_LOGIN_LABEL"), 
165                        _buildI18n("PLUGINS_CORE_USERS_LDAPUSER_CACHE_BY_LOGIN_DESC")),
166                SingleCacheConfiguration.of(
167                        __LDAP_USERDIRECTORY_USER_BY_MAIL_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix(), 
168                        _buildI18n("PLUGINS_CORE_USERS_LDAPUSER_CACHE_BY_MAIL_LABEL"), 
169                        _buildI18n("PLUGINS_CORE_USERS_LDAPUSER_CACHE_BY_MAIL_DESC"))
170        );
171    }
172    
173    private I18nizableText _buildI18n(String i18nKey)
174    {
175        String catalogue = "plugin.core-impl";
176        I18nizableText userDirectoryId = new I18nizableText(getPopulationId() + "#" + getId());
177        Map<String, I18nizableTextParameter> labelParams = Map.of("id", userDirectoryId);
178        return new I18nizableText(catalogue, i18nKey, labelParams);
179    }
180    
181    private Cache<String, User> getCacheByLogin()
182    {
183        return getCache(__LDAP_USERDIRECTORY_USER_BY_LOGIN_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix());
184    }
185    
186    private Cache<String, User> getCacheByMail()
187    {
188        return getCache(__LDAP_USERDIRECTORY_USER_BY_MAIL_CACHE_NAME_PREFIX + getUniqueCacheIdSuffix());
189    }
190    
191    @Override
192    public void init(String id, String udModelId, Map<String, Object> paramValues, String label) throws Exception
193    {
194        _id = id;
195        _udModelId = udModelId;
196        _paramValues = paramValues;
197        _label = label;
198        
199        _usersRelativeDN = (String) paramValues.get(PARAM_USERS_RELATIVE_DN);
200        _usersObjectFilter = (String) paramValues.get(PARAM_USERS_OBJECT_FILTER);
201        _usersSearchScope = ScopeEnumerator.parseScope((String) paramValues.get(PARAM_USERS_SEARCH_SCOPE));
202        _usersLoginAttribute = (String) paramValues.get(PARAM_USERS_LOGIN_ATTRIBUTE);
203        
204        _usersFirstnameAttribute = (String) paramValues.get(PARAM_USERS_FIRSTNAME_ATTRIBUTE);
205        if (_usersFirstnameAttribute != null && _usersFirstnameAttribute.length() == 0)
206        {
207            _usersFirstnameAttribute = null;
208        }
209        
210        _usersLastnameAttribute = (String) paramValues.get(PARAM_USERS_LASTNAME_ATTRIBUTE);
211        _usersEmailAttribute = (String) paramValues.get(PARAM_USERS_EMAIL_ATTRIBUTE);
212        _userEmailIsMandatory = (Boolean) paramValues.get(PARAM_USERS_EMAIL_IS_MANDATORY);
213        
214        String dataSourceId = (String) paramValues.get(PARAM_DATASOURCE_ID);
215        _delayedInitialize(dataSourceId);
216        
217        _pageSize = __DEFAULT_PAGE_SIZE;
218        
219        createCaches();
220    }
221    
222    @Override
223    public void setPopulationId(String populationId)
224    {
225        _populationId = populationId;
226    }
227    
228    @Override
229    public String getPopulationId()
230    {
231        return _populationId;
232    }
233    
234    @Override
235    public Map<String, Object> getParameterValues()
236    {
237        return _paramValues;
238    }
239    
240    @Override
241    public String getUserDirectoryModelId()
242    {
243        return _udModelId;
244    }
245    
246    @Override
247    public Collection<User> getUsers()
248    {
249        // Create a users list
250        List<User> users = new ArrayList<>();
251        try
252        {
253            for (SearchResult result : _search(_pageSize, _usersRelativeDN, _usersObjectFilter, _getSearchConstraint(0), false))
254            {
255                Map<String, Object> attributes = _getAttributes(result);
256                if (attributes != null)
257                {
258                    // Create user
259                    User user = _createUser(attributes);
260                    
261                    if (isCachingEnabled() && user != null)
262                    {
263                        getCacheByLogin().put(user.getIdentity().getLogin(), user);
264                    }
265                    
266                    users.add(user);
267                }
268            }
269        }
270        catch (NamingException e)
271        {
272            getLogger().error("Error of communication with ldap server", e);
273        }
274        catch (IllegalArgumentException e)
275        {
276            getLogger().error("Error missing at least one attribute or attribute value", e);
277        }
278        
279        // Return the users list as a users collection (may be empty)
280        return users;
281    }
282    
283    @Override
284    public List<User> getUsers(int count, int offset, Map<String, Object> parameters)
285    {
286        String pattern = (String) parameters.get("pattern");
287        if (StringUtils.isEmpty(pattern))
288        {
289            pattern = null;
290        }
291        
292        if (count != 0)
293        {
294            Map<String, Map<String, Object>> entries = new LinkedHashMap<>();
295            return _internalGetUsers(entries, count, offset >= 0 ? offset : 0, pattern, 0);
296        }
297        return new ArrayList<>();
298    }
299    
300    @Override
301    public User getUserByEmail(String email) throws NotUniqueUserException
302    {
303        if (isCachingEnabled() && getCacheByMail().hasKey(email))
304        {
305            User user = getCacheByMail().get(email);
306            return user;
307        }
308        
309        User principal = null;
310
311        DirContext context = null;
312        NamingEnumeration<SearchResult> results = null;
313
314        try
315        {
316            // Connection to the LDAP server
317            Hashtable<String, String> contextEnv = _getContextEnv();
318            
319            // For AD with weird references
320            if (!_ldapFollowReferrals)
321            {
322                contextEnv.put(javax.naming.Context.REFERRAL, "throw");
323            }
324
325            context = new InitialDirContext(contextEnv);
326
327            // Escape the login and create the search filter
328            String filter = "(&" + _usersObjectFilter + "(" + _usersEmailAttribute + "={0}))";
329            Object[] params = new Object[] {email};
330
331            // Execute ldap search
332            results = context.search(_usersRelativeDN, filter, params, _getSearchConstraint(0));
333
334            // Search the user
335            if (results.hasMore())
336            {
337                SearchResult result = results.next();
338                Map<String, Object> attributes = _getAttributes(result);
339                if (attributes != null)
340                {
341                    // Add a new user to the list
342                    principal = _createUser(attributes);
343                }
344                
345                // Test if the enumeration has more results with hasMoreElements to avoid unnecessary logs.
346                if (results.hasMoreElements())
347                {
348                    // Cancel the result because there are several matches for one login
349                    throw new NotUniqueUserException("Multiple matches for attribute '" + _usersEmailAttribute + "' and value = '" + email + "'");
350                }
351            }
352
353            if (isCachingEnabled())
354            {
355                getCacheByMail().put(email, principal);
356            }
357        }
358        catch (IllegalArgumentException e)
359        {
360            getLogger().error("Error missing at least one attribute or attribute value for email '" + email + "'", e);
361        }
362        catch (PartialResultException e)
363        {
364            if (_ldapFollowReferrals)
365            {
366                getLogger().debug(String.format("Error communicating with ldap server retrieving user with email '{}'", email), e);
367            }
368            else
369            {
370                getLogger().error("Error communicating with ldap server retrieving user with email '" + email + "'", e);
371            }
372        }
373        catch (NamingException e)
374        {
375            getLogger().error("Error communicating with ldap server retrieving user with email '" + email + "'", e);
376        }
377
378        finally
379        {
380            // Close connections
381            _cleanup(context, results);
382        }
383
384        // Return the users or null
385        return principal;
386    }
387
388    @Override
389    public User getUser(String login)
390    {
391        if (isCachingEnabled() && getCacheByLogin().hasKey(login))
392        {
393            User user = getCacheByLogin().get(login);
394            return user;
395        }
396        
397        User principal = null;
398
399        DirContext context = null;
400        NamingEnumeration<SearchResult> results = null;
401
402        try
403        {
404            // Connection to the LDAP server
405            Hashtable<String, String> contextEnv = _getContextEnv();
406            
407            // For AD with weird references
408            if (!_ldapFollowReferrals)
409            {
410                contextEnv.put(javax.naming.Context.REFERRAL, "throw");
411            }
412
413            context = new InitialDirContext(contextEnv);
414
415            // Escape the login and create the search filter
416            String filter = "(&" + _usersObjectFilter + "(" + _usersLoginAttribute + "={0}))";
417            Object[] params = new Object[] {login};
418
419            // Execute ldap search
420            results = context.search(_usersRelativeDN, filter, params, _getSearchConstraint(0));
421
422            // Search the user
423            while (results.hasMore())
424            {
425                SearchResult result = results.next();
426                Map<String, Object> attributes = _getAttributes(result);
427                if (attributes != null)
428                {
429                    if (!StringUtils.equals(login, (String) attributes.get(_usersLoginAttribute)))
430                    {
431                        // LDAP search may be insensitive, but this method have to be case sensitive
432                        // So we manually check
433                        continue;
434                    }
435
436                    // Test if the enumeration has more results with hasMoreElements to avoid unnecessary logs.
437                    if (principal != null)
438                    {
439                        // Cancel the result because there are several matches for one login
440                        principal = null;
441                        getLogger().error("Multiple matches for attribute '{}' and value = '{}'", _usersLoginAttribute, login);
442                        break;
443                    }
444
445                    // Add a new user to the list
446                    principal = _createUser(attributes);
447                }
448            }
449
450            if (isCachingEnabled())
451            {
452                getCacheByLogin().put(login, principal);
453            }
454        }
455        catch (IllegalArgumentException e)
456        {
457            getLogger().error("Error missing at least one attribute or attribute value for login '" + login + "'", e);
458        }
459        catch (PartialResultException e)
460        {
461            if (_ldapFollowReferrals)
462            {
463                getLogger().debug(String.format("Error communicating with ldap server retrieving user with login '{}'", login), e);
464            }
465            else
466            {
467                getLogger().error("Error communicating with ldap server retrieving user with login '" + login + "'", e);
468            }
469        }
470        catch (NamingException e)
471        {
472            getLogger().error("Error communicating with ldap server retrieving user with login '" + login + "'", e);
473        }
474
475        finally
476        {
477            // Close connections
478            _cleanup(context, results);
479        }
480
481        // Return the users or null
482        return principal;
483    }
484
485    @Override
486    public boolean checkCredentials(String login, String password)
487    {
488        boolean authenticated = false;
489
490        // Check password is not empty
491        if (StringUtils.isNotEmpty(password))
492        {
493            // Retrieve user DN
494            String userDN = getUserDN(login);
495            if (userDN != null)
496            {
497                DirContext context = null;
498
499                // Retrieve connection parameters
500                Hashtable<String, String> env = _getContextEnv();
501
502                // Edit DN and password for authentication
503                env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple");
504                env.put(javax.naming.Context.SECURITY_PRINCIPAL, userDN);
505                env.put(javax.naming.Context.SECURITY_CREDENTIALS, password);
506
507                try
508                {
509                    // Connection and authentication to LDAP server
510                    context = new InitialDirContext(env);
511                    // Authentication succeeded
512                    authenticated = true;
513                }
514                catch (AuthenticationException e)
515                {
516                    getLogger().info("Authentication failed", e);
517                }
518                catch (NamingException e)
519                {
520                    // Error
521                    getLogger().error("Error communication with ldap server", e);
522                }
523                finally
524                {
525                    // Close connections
526                    _cleanup(context, null);
527                }
528            }
529        }
530        else if (getLogger().isDebugEnabled())
531        {
532            getLogger().debug("LDAP Authentication failed since no password (or an empty one) was given");
533        }
534
535        // If an error happened, do not authenticate the user
536        return authenticated;
537    }
538    
539    /**
540     * Get the distinguished name of an user by his login.
541     * @param login Login of the user.
542     * @return The dn of the user, or null if there is no match or if multiple
543     *         matches.
544     */
545    public String getUserDN(String login)
546    {
547        String userDN = null;
548        DirContext context = null;
549        NamingEnumeration<SearchResult> results = null;
550
551        try
552        {
553            // Connection to the LDAP server
554            context = new InitialDirContext(_getContextEnv());
555
556            // Create search filter
557            String filter = "(&" + _usersObjectFilter + "(" + _usersLoginAttribute + "={0}))";
558            Object[] params = new Object[] {login};
559
560            SearchControls constraints = new SearchControls();
561            // Choose depth of parameterized search
562            constraints.setSearchScope(_usersSearchScope);
563            // Do not ask attributes, we only want the DN
564            constraints.setReturningAttributes(new String[] {});
565
566            // Execute ldap search
567            results = context.search(_usersRelativeDN, filter, params, constraints);
568
569            // Fill users list
570            if (results.hasMore())
571            {
572                SearchResult result = results.next();
573
574                // Retrieve the DN
575                userDN = result.getName();
576                if (result.isRelative())
577                {
578                    // Retrieve the absolute DN 
579                    NameParser parser = context.getNameParser("");
580                    Name topDN = parser.parse(context.getNameInNamespace());
581                    topDN.addAll(parser.parse(_usersRelativeDN));
582                    topDN.addAll(parser.parse(userDN));
583                    userDN = topDN.toString();
584                }
585                
586                if (results.hasMoreElements())
587                {
588                    // Cancel the result because there are several matches for one login
589                    userDN = null;
590                    getLogger().error("Multiple matches for attribute \"{}\" and value = \"{}\"", _usersLoginAttribute, login);
591                }
592            }
593        }
594        catch (NamingException e)
595        {
596            getLogger().error("Error communicating with ldap server retrieving user with login '" + login + "'", e);
597        }
598
599        finally
600        {
601            // Close connections
602            _cleanup(context, results);
603        }
604        return userDN;
605    }
606    
607    /**
608     * Create a new user from LDAP attributes
609     * @param attributes the LDAP attributes
610     * @return the user
611     */
612    protected User _createUser(Map<String, Object> attributes)
613    {
614        if (attributes == null)
615        {
616            return null;
617        }
618
619        String login = (String) attributes.get(_usersLoginAttribute);
620        String userDn = (String) attributes.get("userDN");
621        String lastName = (String) attributes.get(_usersLastnameAttribute);
622
623        String firstName = null;
624        if (_usersFirstnameAttribute != null)
625        {
626            firstName = (String) attributes.get(_usersFirstnameAttribute);
627        }
628
629        String email = (String) attributes.get(_usersEmailAttribute);
630
631        return new User(new LdapUserIdentity(login, _populationId, userDn), lastName, firstName, email, this);
632    }
633    
634    /**
635     * Get the user list.
636     * @param entries Where to store entries
637     * @param count The maximum number of users to sax. Cannot be 0. Can be -1 to all.
638     * @param offset The results to ignore
639     * @param pattern The pattern to match.
640     * @param possibleErrors This number will be added to count to set the max of the request, but count results will still be returned. The difference stands for errors.
641     * @return the final offset
642     */
643    protected List<User> _internalGetUsers(Map<String, Map<String, Object>> entries, int count, int offset, String pattern, int possibleErrors)
644    {
645        LdapContext context = null;
646        NamingEnumeration<SearchResult> results = null;
647
648        try
649        {
650            // Connection to the LDAP server
651            context = new InitialLdapContext(_getContextEnv(), null);
652            if (_serverSideSorting)
653            {
654                context.setRequestControls(_getSortControls());
655            }
656
657            Map filter = _getPatternFilter(pattern);
658
659            // Execute ldap search
660            results = context.search(_usersRelativeDN, 
661                                    (String) filter.get("filter"), 
662                                    (Object[]) filter.get("params"), 
663                                    _getSearchConstraint(count == -1 ? 0 : (count + offset + possibleErrors)));
664
665            // Sax results
666            return _users(entries, count, offset, pattern, results, possibleErrors);
667        }
668        catch (IllegalArgumentException e)
669        {
670            getLogger().error("Error missing at least one attribute or value", e);
671            return new ArrayList<>();
672        }
673        catch (NamingException e)
674        {
675            getLogger().error("Error during the communication with ldap server", e);
676            return new ArrayList<>();
677        }
678        finally
679        {
680            // Close connections
681            _cleanup(context, results);
682        }
683    }
684    
685    private List<User> _users(Map<String, Map<String, Object>> entries, int count, int offset, String pattern, NamingEnumeration<SearchResult> results, int possibleErrors)
686    {
687        int nbResults = 0;
688        
689        boolean hasMoreElement = results.hasMoreElements();
690        
691        // First loop on the items to ignore (before the offset)
692        while (nbResults < offset && hasMoreElement)
693        {
694            nbResults++;
695            
696            // FIXME we should check that this element has really attributes to count it as an real offset
697            results.nextElement();
698
699            hasMoreElement = results.hasMoreElements();
700        }
701        
702        // Second loop to work
703        while ((count == -1 || entries.size() < count) && hasMoreElement)
704        {
705            nbResults++;
706            
707            // Next element
708            SearchResult result = results.nextElement();
709            Map<String, Object> attrs = _getAttributes(result);
710            if (attrs != null)
711            {
712                entries.put((String) attrs.get(_usersLoginAttribute), attrs);
713            }
714
715            hasMoreElement = results.hasMoreElements();
716        }
717
718
719        // If we have less results than expected
720        // can be due to errors (null attributes)
721        // can be due to max results is less than wanted results
722        if (entries.size() < count && nbResults == count + offset + possibleErrors)
723        {
724            double nbErrors = count + possibleErrors - entries.size();
725            double askedResultsSize = possibleErrors + count;
726            int newPossibleErrors = Math.max(possibleErrors + count - entries.size(), (int) Math.ceil((nbErrors / askedResultsSize + 1) * nbErrors));
727            return _internalGetUsers(entries, count, offset, pattern, newPossibleErrors);
728        }
729        else
730        {
731            List<User> users = new ArrayList<>();
732            for (Map<String, Object> attributes : entries.values())
733            {
734                users.add(_createUser(attributes));
735            }
736            return users;
737        }
738    }
739    
740    /**
741     * Get the sort control.
742     * @return the sort controls. May be empty if a small error occurs
743     */
744    protected Control[] _getSortControls()
745    {
746        try
747        {
748            SortControl sortControl = new SortControl(getSortByFields(), Control.NONCRITICAL);
749            return new Control[] {sortControl};
750        }
751        catch (IOException e)
752        {
753            getLogger().warn("Cannot sort request on LDAP", e);
754            return new Control[0];
755        }
756    }
757
758    /**
759     * Get the filter from a pattern.
760     * @param pattern The pattern to match.
761     * @return The result as a Map containing the filter and the parameters.
762     */
763    protected Map<String, Object> _getPatternFilter(String pattern)
764    {
765        Map<String, Object> result = new HashMap<>();
766
767        // Check if pattern
768        if (pattern == null)
769        {
770            result.put("filter", _usersObjectFilter);
771            result.put("params", new Object[0]);
772        }
773        else
774        {
775            // Create search filter escaping variables
776            StringBuffer filter = new StringBuffer("(&" + _usersObjectFilter + "(|(");
777            Object[] params = null;
778
779            if (_usersFirstnameAttribute == null)
780            {
781                filter.append(_usersLoginAttribute);
782                filter.append("=*{0}*)(");
783                filter.append(_usersLastnameAttribute);
784                filter.append("=*{1}*)))");
785                params = new Object[] {pattern, pattern};
786            }
787            else
788            {
789                filter.append(_usersLoginAttribute);
790                filter.append("=*{0}*)(");
791                filter.append(_usersFirstnameAttribute);
792                filter.append("=*{1}*)(");
793                filter.append(_usersLastnameAttribute);
794                filter.append("=*{2}*)))");
795                params = new Object[] {pattern, pattern, pattern};
796            }
797
798            result.put("filter", filter.toString());
799            result.put("params", params);
800        }
801        return result;
802    }
803    
804    /**
805     * Get constraints for a search.
806     * @param maxResults The maximum number of items that will be retrieve (0
807     *            means all)
808     * @return The constraints as a SearchControls.
809     */
810    protected SearchControls _getSearchConstraint(int maxResults)
811    {
812        // Search parameters
813        SearchControls constraints = new SearchControls();
814        int attributesCount = 4;
815        int index = 0;
816
817        if (_usersFirstnameAttribute == null)
818        {
819            attributesCount--;
820        }
821
822        // Position the wanted attributes
823        String[] attrs = new String[attributesCount];
824
825        attrs[index++] = _usersLoginAttribute;
826        if (_usersFirstnameAttribute != null)
827        {
828            attrs[index++] = _usersFirstnameAttribute;
829        }
830        attrs[index++] = _usersLastnameAttribute;
831        attrs[index++] = _usersEmailAttribute;
832
833        constraints.setReturningAttributes(attrs);
834
835        // Choose depth of search
836        constraints.setSearchScope(_usersSearchScope);
837        
838        if (maxResults > 0)
839        {
840            constraints.setCountLimit(maxResults);
841        }
842
843        return constraints;
844    }
845    
846    /**
847     * Get the User corresponding to an user ldap entry
848     * @param attributes The ldap attributes of the entry to sax.
849     * @return the JSON representation
850     */
851    @Deprecated
852    protected User _entry2User(Map<String, Object> attributes)
853    {
854        return _createUser(attributes);
855    }
856    
857    /**
858     * Get attributes from a ldap entry.
859     * @param entry The ldap entry to get attributes from.
860     * @return The attributes in a map.
861     * @throws IllegalArgumentException If a needed attribute is missing.
862     */
863    protected Map<String, Object> _getAttributes(SearchResult entry)
864    {
865        try
866        {
867            Map<String, Object> result = new HashMap<>();
868
869            // Retrieve the entry attributes
870            Attributes attrs = entry.getAttributes();
871
872            // Retrieve the DN
873            result.put("userDN", entry.getNameInNamespace());
874            
875            // Retrieve the login
876            Attribute ldapAttr = attrs.get(_usersLoginAttribute);
877            if (ldapAttr == null)
878            {
879                getLogger().warn("Missing login attribute : '{}'", _usersLoginAttribute);
880                return null;
881            }
882            result.put(_usersLoginAttribute, ldapAttr.get());
883            
884            if (_usersFirstnameAttribute != null)
885            {
886                // Retrieve the first name
887                ldapAttr = attrs.get(_usersFirstnameAttribute);
888                if (ldapAttr == null)
889                {
890                    getLogger().warn("Missing firstname attribute : '{}', for user '{}'.", _usersFirstnameAttribute, result.get(_usersLoginAttribute));
891                    return null;
892                }
893                result.put(_usersFirstnameAttribute, ldapAttr.get());
894            }
895
896            // Retrieve the last name
897            ldapAttr = attrs.get(_usersLastnameAttribute);
898            if (ldapAttr == null)
899            {
900                getLogger().warn("Missing lastname attribute : '{}', for user '{}'.", _usersLastnameAttribute, result.get(_usersLoginAttribute));
901                return null;
902            }
903            result.put(_usersLastnameAttribute, ldapAttr.get());
904
905            // Retrieve the email
906            ldapAttr = attrs.get(_usersEmailAttribute);
907            if (ldapAttr == null && _userEmailIsMandatory)
908            {
909                getLogger().warn("Missing email attribute : '{}', for user '{}'.", _usersEmailAttribute, result.get(_usersLoginAttribute));
910                return null;
911            }
912
913            if (ldapAttr == null)
914            {
915                result.put(_usersEmailAttribute, "");
916            }
917            else
918            {
919                result.put(_usersEmailAttribute, ldapAttr.get());
920            }
921
922            return result;
923        }
924        catch (NamingException e)
925        {
926            throw new IllegalArgumentException("Missing at least one value for an attribute in an ldap entry", e);
927        }
928    }
929
930    @Override
931    protected String[] getSortByFields()
932    {
933        return new String[] {_usersLastnameAttribute, _usersFirstnameAttribute};
934    }
935    
936}