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