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