001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019
020package org.apache.james.user.ldap;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.Properties;
029import java.util.Set;
030
031import javax.annotation.PostConstruct;
032import javax.naming.Context;
033import javax.naming.NamingEnumeration;
034import javax.naming.NamingException;
035import javax.naming.directory.Attribute;
036import javax.naming.directory.Attributes;
037import javax.naming.directory.SearchControls;
038import javax.naming.directory.SearchResult;
039import javax.naming.ldap.InitialLdapContext;
040import javax.naming.ldap.LdapContext;
041
042import org.apache.commons.configuration.ConfigurationException;
043import org.apache.commons.configuration.HierarchicalConfiguration;
044import org.apache.commons.lang.StringUtils;
045import org.apache.james.lifecycle.api.Configurable;
046import org.apache.james.lifecycle.api.LogEnabled;
047import org.apache.james.user.api.UsersRepository;
048import org.apache.james.user.api.UsersRepositoryException;
049import org.apache.james.user.api.model.User;
050import org.apache.james.user.ldap.api.LdapConstants;
051import org.apache.james.util.retry.DoublingRetrySchedule;
052import org.apache.james.util.retry.api.RetrySchedule;
053import org.apache.james.util.retry.naming.ldap.RetryingLdapContext;
054import org.apache.mailet.MailAddress;
055import org.slf4j.Logger;
056
057import com.google.common.base.Optional;
058
059/**
060 * <p>
061 * This repository implementation serves as a bridge between Apache James and
062 * LDAP. It allows James to authenticate users against an LDAP compliant server
063 * such as Apache DS or Microsoft AD. It also enables role/group based access
064 * restriction based on LDAP groups.
065 * </p>
066 * <p>
067 * It is intended for organisations that already have a user-authentication and
068 * authorisation mechanism in place, and want to leverage this when deploying
069 * James. The assumption inherent here is that such organisations would not want
070 * to manage user details via James, but will do so externally using whatever
071 * mechanism provided by, or built on top off, their LDAP implementation.
072 * </p>
073 * <p>
074 * Based on this assumption, this repository is strictly <b>read-only</b>. As a
075 * consequence, user modification, deletion and creation requests will be
076 * ignored when using this repository.
077 * </p>
078 * <p>
079 * The following fragment of XML provides an example configuration to enable
080 * this repository: </br>
081 *
082 * <pre>
083 *  &lt;users-store&gt;
084 *      &lt;repository name=&quot;LDAPUsers&quot;
085 *      class=&quot;org.apache.james.userrepository.ReadOnlyUsersLDAPRepository&quot;
086 *      ldapHost=&quot;ldap://myldapserver:389&quot;
087 *      principal=&quot;uid=ldapUser,ou=system&quot;
088 *      credentials=&quot;password&quot;
089 *      userBase=&quot;ou=People,o=myorg.com,ou=system&quot;
090 *      userIdAttribute=&quot;uid&quot;
091 *      userObjectClass=&quot;inetOrgPerson&quot;
092 *      maxRetries=&quot;20&quot;
093 *      retryStartInterval=&quot;0&quot;
094 *      retryMaxInterval=&quot;30&quot;
095 *      retryIntervalScale=&quot;1000&quot;
096 *      administratorId=&quot;ldapAdmin&quot;
097 *  &lt;/users-store&gt;
098 * </pre>
099 *
100 * </br>
101 *
102 * Its constituent attributes are defined as follows:
103 * <ul>
104 * <li><b>ldapHost:</b> The URL of the LDAP server to connect to.</li>
105 * <li>
106 * <b>principal:</b> (optional) The name (DN) of the user with which to
107 * initially bind to the LDAP server.</li>
108 * <li>
109 * <b>credentials:</b> (optional) The password with which to initially bind to
110 * the LDAP server.</li>
111 * <li>
112 * <b>userBase:</b>The context within which to search for user entities.</li>
113 * <li>
114 * <b>userIdAttribute:</b>The name of the LDAP attribute which holds user ids.
115 * For example &quot;uid&quot; for Apache DS, or &quot;sAMAccountName&quot; for
116 * Microsoft Active Directory.</li>
117 * <li>
118 * <b>userObjectClass:</b>The objectClass value for user nodes below the
119 * userBase. For example &quot;inetOrgPerson&quot; for Apache DS, or
120 * &quot;user&quot; for Microsoft Active Directory.</li>
121 **
122 * <li>
123 * <b>maxRetries:</b> (optional, default = 0) The maximum number of times to
124 * retry a failed operation. -1 means retry forever.</li>
125 * <li>
126 * <b>retryStartInterval:</b> (optional, default = 0) The interval in
127 * milliseconds to wait before the first retry. If > 0, subsequent retries are
128 * made at double the proceeding one up to the <b>retryMaxInterval</b> described
129 * below. If = 0, the next retry is 1 and subsequent retries proceed as above.</li>
130 * <li>
131 * <b>retryMaxInterval:</b> (optional, default = 60) The maximum interval in
132 * milliseconds to wait between retries</li>
133 * <li>
134 * <b>retryIntervalScale:</b> (optional, default = 1000) The amount by which to
135 * multiply each retry interval. The default value of 1000 (milliseconds) is 1
136 * second, so the default <b>retryMaxInterval</b> of 60 is 60 seconds, or 1
137 * minute.
138 * </ul>
139 * </p>
140 * <p>
141 * <em>Example Schedules</em>
142 * <ul>
143 * <li>
144 * Retry after 1000 milliseconds, doubling the interval for each retry up to
145 * 30000 milliseconds, subsequent retry intervals are 30000 milliseconds until
146 * 10 retries have been attempted, after which the <code>Exception</code>
147 * causing the fault is thrown:
148 * <ul>
149 * <li>maxRetries = 10
150 * <li>retryStartInterval = 1000
151 * <li>retryMaxInterval = 30000
152 * <li>retryIntervalScale = 1
153 * </ul>
154 * <li>
155 * Retry immediately, then retry after 1 * 1000 milliseconds, doubling the
156 * interval for each retry up to 30 * 1000 milliseconds, subsequent retry
157 * intervals are 30 * 1000 milliseconds until 20 retries have been attempted,
158 * after which the <code>Exception</code> causing the fault is thrown:
159 * <ul>
160 * <li>maxRetries = 20
161 * <li>retryStartInterval = 0
162 * <li>retryMaxInterval = 30
163 * <li>retryIntervalScale = 1000
164 * </ul>
165 * <li>
166 * Retry after 5000 milliseconds, subsequent retry intervals are 5000
167 * milliseconds. Retry forever:
168 * <ul>
169 * <li>maxRetries = -1
170 * <li>retryStartInterval = 5000
171 * <li>retryMaxInterval = 5000
172 * <li>retryIntervalScale = 1
173 * </ul>
174 * </ul>
175 * </p>
176 *
177 * <p>
178 * In order to enable group/role based access restrictions, you can use the
179 * &quot;&lt;restriction&gt;&quot; configuration element. An example of this is
180 * shown below: <br>
181 *
182 * <pre>
183 * &lt;restriction
184 *  memberAttribute=&quot;uniqueMember&quot;&gt;
185 *    &lt;group&gt;cn=PermanentStaff,ou=Groups,o=myorg.co.uk,ou=system&lt;/group&gt;
186 *          &lt;group&gt;cn=TemporaryStaff,ou=Groups,o=myorg.co.uk,ou=system&lt;/group&gt;
187 * &lt;/restriction&gt;
188 * </pre>
189 *
190 * Its constituent attributes and elements are defined as follows:
191 * <ul>
192 * <li>
193 * <b>memberAttribute:</b> The LDAP attribute whose values indicate the DNs of
194 * the users which belong to the group or role.</li>
195 * <li>
196 * <b>group:</b> A valid group or role DN. A user is only authenticated
197 * (permitted access) if they belong to at least one of the groups listed under
198 * the &quot;&lt;restriction&gt;&quot; sections.</li>
199 * </ul>
200 * </p>
201 *
202 * <p>
203 * The following parameters may be used to adjust the underlying
204 * <code>com.sun.jndi.ldap.LdapCtxFactory</code>. See <a href=
205 * "http://docs.oracle.com/javase/1.5.0/docs/guide/jndi/jndi-ldap.html#SPIPROPS"
206 * > LDAP Naming Service Provider for the Java Naming and Directory InterfaceTM
207 * (JNDI) : Provider-specific Properties</a> for details.
208 * <ul>
209 * <li>
210 * <b>useConnectionPool:</b> (optional, default = true) Sets property
211 * <code>com.sun.jndi.ldap.connect.pool</code> to the specified boolean value
212 * <li>
213 * <b>connectionTimeout:</b> (optional) Sets property
214 * <code>com.sun.jndi.ldap.connect.timeout</code> to the specified integer value
215 * <li>
216 * <b>readTimeout:</b> (optional) Sets property
217 * <code>com.sun.jndi.ldap.read.timeout</code> to the specified integer value.
218 * Applicable to Java 6 and above.
219 * <li>
220 * <b>administratorId:</b> (optional) User identifier of the administrator user.
221 * The administrator user is allowed to authenticate as other users.
222 * </ul>
223 * </p>
224 *
225 * <p>
226 * The <b>supportsVirtualHosting</b> tag allows you to define this repository as supporing
227 * virtual hosting. For this LDAP repository, it means users will be looked for by their email
228 * address instead of their unique identifier.
229 * Generally to make it work, you need to configure <b>userIdAttribute</b> attribute to map
230 * to a mail attribute such as <code>mail</code> instead of an unique id identifier.
231 * </p>
232 *
233 * @see ReadOnlyLDAPUser
234 * @see ReadOnlyLDAPGroupRestriction
235 *
236 */
237public class ReadOnlyUsersLDAPRepository implements UsersRepository, Configurable, LogEnabled {
238
239    // The name of the factory class which creates the initial context
240    // for the LDAP service provider
241    private static final String INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
242
243    private static final String PROPERTY_NAME_CONNECTION_POOL = "com.sun.jndi.ldap.connect.pool";
244    private static final String PROPERTY_NAME_CONNECT_TIMEOUT = "com.sun.jndi.ldap.connect.timeout";
245    private static final String PROPERTY_NAME_READ_TIMEOUT = "com.sun.jndi.ldap.read.timeout";
246    public static final String SUPPORTS_VIRTUAL_HOSTING = "supportsVirtualHosting";
247
248    /**
249     * The URL of the LDAP server against which users are to be authenticated.
250     * Note that users are actually authenticated by binding against the LDAP
251     * server using the users &quot;dn&quot; and &quot;credentials&quot;.The
252     * value of this field is taken from the value of the configuration
253     * attribute &quot;ldapHost&quot;.
254     */
255    private String ldapHost;
256
257    /**
258     * The value of this field is taken from the configuration attribute
259     * &quot;userIdAttribute&quot;. This is the LDAP attribute type which holds
260     * the userId value. Note that this is not the same as the email address
261     * attribute.
262     */
263    private String userIdAttribute;
264
265    /**
266     * The value of this field is taken from the configuration attribute
267     * &quot;userObjectClass&quot;. This is the LDAP object class to use in the
268     * search filter for user nodes under the userBase value.
269     */
270    private String userObjectClass;
271
272    /**
273     * The value of this field is taken from the configuration attribute &quot;filter&quot;.
274     * This is the search filter to use to find the desired user. 
275     */
276    private String filter;
277    
278    /**
279     * This is the LDAP context/sub-context within which to search for user
280     * entities. The value of this field is taken from the configuration
281     * attribute &quot;userBase&quot;.
282     */
283    private String userBase;
284
285    /**
286     * The user with which to initially bind to the LDAP server. The value of
287     * this field is taken from the configuration attribute
288     * &quot;principal&quot;.
289     */
290    private String principal;
291
292    /**
293     * The password/credentials with which to initially bind to the LDAP server.
294     * The value of this field is taken from the configuration attribute
295     * &quot;credentials&quot;.
296     */
297    private String credentials;
298
299    /**
300     * Encapsulates the information required to restrict users to LDAP groups or
301     * roles. This object is populated from the contents of the configuration
302     * element &lt;restriction&gt;.
303     */
304    private ReadOnlyLDAPGroupRestriction restriction;
305
306    /**
307     * The context for the LDAP server. This is the connection that is built
308     * from the configuration attributes &quot;ldapHost&quot;,
309     * &quot;principal&quot; and &quot;credentials&quot;.
310     */
311    private LdapContext ldapContext;
312    private boolean supportsVirtualHosting;
313    
314    /**
315     * UserId of the administrator
316     * The administrator is allowed to log in as other users
317     */
318    private Optional<String> administratorId;
319
320    // Use a connection pool. Default is true.
321    private boolean useConnectionPool = true;
322
323    // The connection timeout in milliseconds.
324    // A value of less than or equal to zero means to use the network protocol's
325    // (i.e., TCP's) timeout value.
326    private int connectionTimeout = -1;
327
328    // The LDAP read timeout in milliseconds.
329    private int readTimeout = -1;
330
331    // The schedule for retry attempts
332    private RetrySchedule schedule = null;
333
334    // Maximum number of times to retry a connection attempts. Default is no
335    // retries.
336    private int maxRetries = 0;
337
338    private Logger log;
339
340    /**
341     * Creates a new instance of ReadOnlyUsersLDAPRepository.
342     *
343     */
344    public ReadOnlyUsersLDAPRepository() {
345        super();
346    }
347
348    /**
349     * Extracts the parameters required by the repository instance from the
350     * James server configuration data. The fields extracted include
351     * {@link #ldapHost}, {@link #userIdAttribute}, {@link #userBase},
352     * {@link #principal}, {@link #credentials} and {@link #restriction}.
353     *
354     * @param configuration
355     *            An encapsulation of the James server configuration data.
356     */
357    public void configure(HierarchicalConfiguration configuration) throws ConfigurationException {
358        ldapHost = configuration.getString("[@ldapHost]", "");
359        principal = configuration.getString("[@principal]", "");
360        credentials = configuration.getString("[@credentials]", "");
361        userBase = configuration.getString("[@userBase]");
362        userIdAttribute = configuration.getString("[@userIdAttribute]");
363        userObjectClass = configuration.getString("[@userObjectClass]");
364        // Default is to use connection pooling
365        useConnectionPool = configuration.getBoolean("[@useConnectionPool]", true);
366        connectionTimeout = configuration.getInt("[@connectionTimeout]", -1);
367        readTimeout = configuration.getInt("[@readTimeout]", -1);
368        // Default maximum retries is 1, which allows an alternate connection to
369        // be found in a multi-homed environment
370        maxRetries = configuration.getInt("[@maxRetries]", 1);
371        supportsVirtualHosting = configuration.getBoolean(SUPPORTS_VIRTUAL_HOSTING, false);
372        // Default retry start interval is 0 second
373        long retryStartInterval = configuration.getLong("[@retryStartInterval]", 0);
374        // Default maximum retry interval is 60 seconds
375        long retryMaxInterval = configuration.getLong("[@retryMaxInterval]", 60);
376        int scale = configuration.getInt("[@retryIntervalScale]", 1000); // seconds
377        schedule = new DoublingRetrySchedule(retryStartInterval, retryMaxInterval, scale);
378
379        HierarchicalConfiguration restrictionConfig = null;
380        // Check if we have a restriction we can use
381        // See JAMES-1204
382        if (configuration.containsKey("restriction[@memberAttribute]")) {
383            restrictionConfig = configuration.configurationAt("restriction");
384        }
385        restriction = new ReadOnlyLDAPGroupRestriction(restrictionConfig);
386
387        //see if there is a filter argument
388        filter = configuration.getString("[@filter]");
389
390        administratorId = Optional.fromNullable(configuration.getString("[@administratorId]"));
391
392        checkState();
393    }
394
395    private void checkState() throws ConfigurationException {
396        if (userBase == null) {
397            throw new ConfigurationException("[@userBase] is mandatory");
398        }
399        if (userIdAttribute == null) {
400            throw new ConfigurationException("[@userIdAttribute] is mandatory");
401        }
402        if (userObjectClass == null) {
403            throw new ConfigurationException("[@userObjectClass] is mandatory");
404        }
405    }
406
407    /**
408     * Initialises the user-repository instance. It will create a connection to
409     * the LDAP host using the supplied configuration.
410     *
411     * @throws Exception
412     *             If an error occurs authenticating or connecting to the
413     *             specified LDAP host.
414     */
415    @PostConstruct
416    public void init() throws Exception {
417        if (log.isDebugEnabled()) {
418            log.debug(this.getClass().getName() + ".init()" + '\n' + "LDAP host: " + ldapHost + '\n' + "User baseDN: " + userBase + '\n' + "userIdAttribute: " + userIdAttribute + '\n' + "Group restriction: " + restriction + '\n' + "UseConnectionPool: " + useConnectionPool + '\n' + "connectionTimeout: " + connectionTimeout + '\n' + "readTimeout: " + readTimeout + '\n' + "retrySchedule: " + schedule + '\n' + "maxRetries: " + maxRetries + '\n');
419        }
420        // Setup the initial LDAP context
421        updateLdapContext();
422    }
423
424    /**
425     * Answer the LDAP context used to connect with the LDAP server.
426     *
427     * @return an <code>LdapContext</code>
428     * @throws NamingException
429     */
430    protected LdapContext getLdapContext() throws NamingException {
431        if (null == ldapContext) {
432            updateLdapContext();
433        }
434        return ldapContext;
435    }
436
437    protected void updateLdapContext() throws NamingException {
438        ldapContext = computeLdapContext();
439    }
440
441    /**
442     * Answers a new LDAP/JNDI context using the specified user credentials.
443     *
444     * @return an LDAP directory context
445     * @throws NamingException
446     *             Propagated from underlying LDAP communication API.
447     */
448    protected LdapContext computeLdapContext() throws NamingException {
449        return new RetryingLdapContext(schedule, maxRetries, log) {
450
451            @Override
452            public Context newDelegate() throws NamingException {
453                return new InitialLdapContext(getContextEnvironment(), null);
454            }
455        };
456    }
457
458    protected Properties getContextEnvironment()
459    {
460        final Properties props = new Properties();
461        props.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
462        props.put(Context.PROVIDER_URL, null == ldapHost ? "" : ldapHost);
463        if (null == credentials || credentials.isEmpty()) {
464            props.put(Context.SECURITY_AUTHENTICATION, LdapConstants.SECURITY_AUTHENTICATION_NONE);
465        } else {
466            props.put(Context.SECURITY_AUTHENTICATION, LdapConstants.SECURITY_AUTHENTICATION_SIMPLE);
467            props.put(Context.SECURITY_PRINCIPAL, null == principal ? "" : principal);
468            props.put(Context.SECURITY_CREDENTIALS, credentials);
469        }
470        // The following properties are specific to com.sun.jndi.ldap.LdapCtxFactory
471        props.put(PROPERTY_NAME_CONNECTION_POOL, Boolean.toString(useConnectionPool));
472        if (connectionTimeout > -1)
473        {
474            props.put(PROPERTY_NAME_CONNECT_TIMEOUT, Integer.toString(connectionTimeout));
475        }
476        if (readTimeout > -1)
477        {
478            props.put(PROPERTY_NAME_READ_TIMEOUT, Integer.toString(readTimeout));
479        }
480        return props;
481    }
482
483    /**
484     * Indicates if the user with the specified DN can be found in the group
485     * membership map&#45;as encapsulated by the specified parameter map.
486     *
487     * @param userDN
488     *            The DN of the user to search for.
489     * @param groupMembershipList
490     *            A map containing the entire group membership lists for the
491     *            configured groups. This is organised as a map of
492     *
493     *            <code>&quot;&lt;groupDN&gt;=&lt;[userDN1,userDN2,...,userDNn]&gt;&quot;</code>
494     *            pairs. In essence, each <code>groupDN</code> string is
495     *            associated to a list of <code>userDNs</code>.
496     * @return <code>True</code> if the specified userDN is associated with at
497     *         least one group in the parameter map, and <code>False</code>
498     *         otherwise.
499     */
500    private boolean userInGroupsMembershipList(String userDN,
501            Map<String, Collection<String>> groupMembershipList) {
502        boolean result = false;
503
504        Collection<Collection<String>> memberLists = groupMembershipList.values();
505        Iterator<Collection<String>> memberListsIterator = memberLists.iterator();
506
507        while (memberListsIterator.hasNext() && !result) {
508            Collection<String> groupMembers = memberListsIterator.next();
509            result = groupMembers.contains(userDN);
510        }
511
512        return result;
513    }
514
515    /**
516     * Gets all the user entities taken from the LDAP server, as taken from the
517     * search-context given by the value of the attribute {@link #userBase}.
518     *
519     * @return A set containing all the relevant users found in the LDAP
520     *         directory.
521     * @throws NamingException
522     *             Propagated from the LDAP communication layer.
523     */
524    private Set<String> getAllUsersFromLDAP() throws NamingException {
525        Set<String> result = new HashSet<String>();
526
527        SearchControls sc = new SearchControls();
528        sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
529        sc.setReturningAttributes(new String[] { "distinguishedName" });
530        NamingEnumeration<SearchResult> sr = ldapContext.search(userBase, "(objectClass="
531                + userObjectClass + ")", sc);
532        while (sr.hasMore()) {
533            SearchResult r = sr.next();
534            result.add(r.getNameInNamespace());
535        }
536
537        return result;
538    }
539
540    /**
541     * Extract the user attributes for the given collection of userDNs, and
542     * encapsulates the user list as a collection of {@link ReadOnlyLDAPUser}s.
543     * This method delegates the extraction of a single user's details to the
544     * method {@link #buildUser(String)}.
545     *
546     * @param userDNs
547     *            The distinguished-names (DNs) of the users whose information
548     *            is to be extracted from the LDAP repository.
549     * @return A collection of {@link ReadOnlyLDAPUser}s as taken from the LDAP
550     *         server.
551     * @throws NamingException
552     *             Propagated from the underlying LDAP communication layer.
553     */
554    private Collection<ReadOnlyLDAPUser> buildUserCollection(Collection<String> userDNs)
555            throws NamingException {
556        List<ReadOnlyLDAPUser> results = new ArrayList<ReadOnlyLDAPUser>();
557
558        for (String userDN : userDNs) {
559            ReadOnlyLDAPUser user = buildUser(userDN);
560            results.add(user);
561        }
562
563        return results;
564    }
565
566
567    /**
568     * For a given name, this method makes ldap search in userBase with filter {@link #userIdAttribute}=name and objectClass={@link #userObjectClass}
569     * and builds {@link User} based on search result.
570     *
571     * @param name
572     *            The userId which should be value of the field {@link #userIdAttribute}
573     * @return A {@link ReadOnlyLDAPUser} instance which is initialized with the
574     *         userId of this user and ldap connection information with which
575     *         the user was searched. Return null if such a user was not found.
576     * @throws NamingException
577     *             Propagated by the underlying LDAP communication layer.
578     */
579    private ReadOnlyLDAPUser searchAndBuildUser(String name) throws NamingException {
580      SearchControls sc = new SearchControls();
581      sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
582      sc.setReturningAttributes(new String[] { userIdAttribute });
583      sc.setCountLimit(1);
584
585      StringBuilder builderFilter = new StringBuilder("(&(");
586      builderFilter.append(userIdAttribute).append("=").append(name).append(")")
587                   .append("(objectClass=").append(userObjectClass).append(")");
588
589     if(StringUtils.isNotEmpty(filter)){
590         builderFilter.append(filter).append(")");
591         }
592     else{
593         builderFilter.append(")");
594     }
595
596      NamingEnumeration<SearchResult> sr = ldapContext.search(userBase, builderFilter.toString(),
597          sc);
598
599      if (!sr.hasMore())
600        return null;
601
602      SearchResult r = sr.next();
603      Attribute userName = r.getAttributes().get(userIdAttribute);
604
605      if (!restriction.isActivated()
606          || userInGroupsMembershipList(r.getNameInNamespace(), restriction.getGroupMembershipLists(ldapContext)))
607        return new ReadOnlyLDAPUser(userName.get().toString(), r.getNameInNamespace(), ldapContext);
608
609      return null;
610    }
611
612    /**
613     * Given a userDN, this method retrieves the user attributes from the LDAP
614     * server, so as to extract the items that are of interest to James.
615     * Specifically it extracts the userId, which is extracted from the LDAP
616     * attribute whose name is given by the value of the field
617     * {@link #userIdAttribute}.
618     *
619     * @param userDN
620     *            The distinguished-name of the user whose details are to be
621     *            extracted from the LDAP repository.
622     * @return A {@link ReadOnlyLDAPUser} instance which is initialized with the
623     *         userId of this user and ldap connection information with which
624     *         the userDN and attributes were obtained.
625     * @throws NamingException
626     *             Propagated by the underlying LDAP communication layer.
627     */
628    private ReadOnlyLDAPUser buildUser(String userDN) throws NamingException {
629      Attributes userAttributes = ldapContext.getAttributes(userDN);
630      Attribute userName = userAttributes.get(userIdAttribute);
631      return new ReadOnlyLDAPUser(userName.get().toString(), userDN, ldapContext);
632    }
633
634    /**
635     * @see UsersRepository#contains(java.lang.String)
636     */
637    public boolean contains(String name) throws UsersRepositoryException {
638        return getUserByName(name) != null;
639    }
640
641    /*
642     * TODO Should this be deprecated? At least the method isn't declared in the
643     * interface anymore
644     *
645     * @see UsersRepository#containsCaseInsensitive(java.lang.String)
646     */
647    public boolean containsCaseInsensitive(String name) throws UsersRepositoryException {
648        return getUserByNameCaseInsensitive(name) != null;
649    }
650
651    /**
652     * @see UsersRepository#countUsers()
653     */
654    public int countUsers() throws UsersRepositoryException {
655        try {
656            return getValidUsers().size();
657        } catch (NamingException e) {
658            log.error("Unable to retrieve user count from ldap", e);
659            throw new UsersRepositoryException("Unable to retrieve user count from ldap", e);
660
661        }
662    }
663
664    /*
665     * TODO Should this be deprecated? At least the method isn't declared in the
666     * interface anymore
667     *
668     * @see UsersRepository#getRealName(java.lang.String)
669     */
670    public String getRealName(String name) throws UsersRepositoryException {
671        User u = getUserByNameCaseInsensitive(name);
672        if (u != null) {
673            return u.getUserName();
674        }
675
676        return null;
677    }
678
679    /**
680     * @see UsersRepository#getUserByName(java.lang.String)
681     */
682    public User getUserByName(String name) throws UsersRepositoryException {
683        try {
684          return searchAndBuildUser(name);
685        } catch (NamingException e) {
686            log.error("Unable to retrieve user from ldap", e);
687            throw new UsersRepositoryException("Unable to retrieve user from ldap", e);
688
689        }
690    }
691
692    /*
693     * TODO Should this be deprecated? At least the method isn't declared in the
694     * interface anymore
695     *
696     * @see UsersRepository#getUserByNameCaseInsensitive(java.lang.String)
697     */
698    public User getUserByNameCaseInsensitive(String name) throws UsersRepositoryException {
699        try {
700            for (ReadOnlyLDAPUser u : buildUserCollection(getValidUsers())) {
701                if (u.getUserName().equalsIgnoreCase(name)) {
702                    return u;
703                }
704            }
705
706        } catch (NamingException e) {
707            log.error("Unable to retrieve user from ldap", e);
708            throw new UsersRepositoryException("Unable to retrieve user from ldap", e);
709
710        }
711        return null;
712    }
713
714    /**
715     * @see UsersRepository#list()
716     */
717    public Iterator<String> list() throws UsersRepositoryException {
718        List<String> result = new ArrayList<String>();
719        try {
720
721            for (ReadOnlyLDAPUser readOnlyLDAPUser : buildUserCollection(getValidUsers())) {
722                result.add(readOnlyLDAPUser.getUserName());
723            }
724        } catch (NamingException namingException) {
725            throw new UsersRepositoryException(
726                    "Unable to retrieve users list from LDAP due to unknown naming error.",
727                    namingException);
728        }
729
730        return result.iterator();
731    }
732
733    private Collection<String> getValidUsers() throws NamingException {
734        Set<String> userDNs = getAllUsersFromLDAP();
735        Collection<String> validUserDNs;
736
737        if (restriction.isActivated()) {
738            Map<String, Collection<String>> groupMembershipList = restriction
739                    .getGroupMembershipLists(ldapContext);
740            validUserDNs = new ArrayList<String>();
741
742            Iterator<String> userDNIterator = userDNs.iterator();
743            String userDN;
744            while (userDNIterator.hasNext()) {
745                userDN = userDNIterator.next();
746                if (userInGroupsMembershipList(userDN, groupMembershipList))
747                    validUserDNs.add(userDN);
748            }
749        } else {
750            validUserDNs = userDNs;
751        }
752        return validUserDNs;
753    }
754
755    /**
756     * @see UsersRepository#removeUser(java.lang.String)
757     */
758    public void removeUser(String name) throws UsersRepositoryException {
759        log.warn("This user-repository is read-only. Modifications are not permitted.");
760        throw new UsersRepositoryException(
761                "This user-repository is read-only. Modifications are not permitted.");
762
763    }
764
765    /**
766     * @see UsersRepository#test(java.lang.String, java.lang.String)
767     */
768    public boolean test(String name, String password) throws UsersRepositoryException {
769        User u = getUserByName(name);
770        return u != null && u.verifyPassword(password);
771    }
772
773    /**
774     * @see UsersRepository#addUser(java.lang.String, java.lang.String)
775     */
776    public void addUser(String username, String password) throws UsersRepositoryException {
777        log.error("This user-repository is read-only. Modifications are not permitted.");
778        throw new UsersRepositoryException(
779                "This user-repository is read-only. Modifications are not permitted.");
780    }
781
782    /**
783     */
784    public void updateUser(User user) throws UsersRepositoryException {
785        log.error("This user-repository is read-only. Modifications are not permitted.");
786        throw new UsersRepositoryException(
787                "This user-repository is read-only. Modifications are not permitted.");
788    }
789
790    /**
791     * @see org.apache.james.lifecycle.api.LogEnabled#setLog(org.slf4j.Logger)
792     */
793    public void setLog(Logger log) {
794        this.log = log;
795    }
796
797    /**
798     * VirtualHosting not supported
799     */
800    public boolean supportVirtualHosting() {
801        return supportsVirtualHosting;
802    }
803
804
805    @Override
806    public String getUser(MailAddress mailAddress) throws UsersRepositoryException {
807        if (supportVirtualHosting()) {
808            return mailAddress.asString();
809        } else {
810            return mailAddress.getLocalPart();
811        }
812    }
813
814    @Override
815    public boolean isAdministrator(String username) throws UsersRepositoryException {
816        if (administratorId.isPresent()) {
817            return administratorId.get().equals(username);
818        }
819        return false;
820    }
821}