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