001    package org.apache.turbine.services.security.ldap;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one
005     * or more contributor license agreements.  See the NOTICE file
006     * distributed with this work for additional information
007     * regarding copyright ownership.  The ASF licenses this file
008     * to you under the Apache License, Version 2.0 (the
009     * "License"); you may not use this file except in compliance
010     * with the License.  You may obtain a copy of the License at
011     *
012     *   http://www.apache.org/licenses/LICENSE-2.0
013     *
014     * Unless required by applicable law or agreed to in writing,
015     * software distributed under the License is distributed on an
016     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017     * KIND, either express or implied.  See the License for the
018     * specific language governing permissions and limitations
019     * under the License.
020     */
021    
022    import java.util.List;
023    import java.util.Hashtable;
024    import java.util.Vector;
025    
026    import javax.naming.AuthenticationException;
027    import javax.naming.Context;
028    import javax.naming.NamingEnumeration;
029    import javax.naming.NamingException;
030    import javax.naming.directory.Attributes;
031    import javax.naming.directory.DirContext;
032    import javax.naming.directory.SearchControls;
033    import javax.naming.directory.SearchResult;
034    
035    import org.apache.commons.configuration.Configuration;
036    
037    import org.apache.turbine.om.security.User;
038    import org.apache.turbine.services.security.TurbineSecurity;
039    import org.apache.turbine.services.security.UserManager;
040    import org.apache.turbine.util.security.DataBackendException;
041    import org.apache.turbine.util.security.EntityExistsException;
042    import org.apache.turbine.util.security.PasswordMismatchException;
043    import org.apache.turbine.util.security.UnknownEntityException;
044    
045    /**
046     * A UserManager performs {@link org.apache.turbine.om.security.User}
047     * object related tasks on behalf of the
048     * {@link org.apache.turbine.services.security.SecurityService}.
049     *
050     * This implementation uses ldap for retrieving user data. It
051     * expects that the User interface implementation will be castable to
052     * {@link org.apache.turbine.om.BaseObject}.
053     *
054     * @author <a href="mailto:jon@collab.net">Jon S. Stevens</a>
055     * @author <a href="mailto:john.mcnally@clearink.com">John D. McNally</a>
056     * @author <a href="mailto:frank.kim@clearink.com">Frank Y. Kim</a>
057     * @author <a href="mailto:cberry@gluecode.com">Craig D. Berry</a>
058     * @author <a href="mailto:Rafal.Krzewski@e-point.pl">Rafal Krzewski</a>
059     * @author <a href="mailto:tadewunmi@gluecode.com">Tracy M. Adewunmi</a>
060     * @author <a href="mailto:lflournoy@gluecode.com">Leonard J. Flournoy</a>
061     * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
062     * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
063     * @author <a href="mailto:hhernandez@itweb.com.mx">Humberto Hernandez</a>
064     * @version $Id: LDAPUserManager.java 1096130 2011-04-23 10:37:19Z ludwig $
065     */
066    public class LDAPUserManager implements UserManager
067    {
068        /**
069         * Initializes the UserManager
070         *
071         * @param conf A Configuration object to init this Manager
072         */
073        public void init(Configuration conf)
074        {
075            // GNDN
076        }
077    
078        /**
079         * Check wether a specified user's account exists.
080         *
081         * The login name is used for looking up the account.
082         *
083         * @param user The user to be checked.
084         * @return true if the specified account exists
085         * @throws DataBackendException Error accessing the data backend.
086         */
087        public boolean accountExists(User user) throws DataBackendException
088        {
089            return accountExists(user.getName());
090        }
091    
092        /**
093         *
094         * Check wether a specified user's account exists.
095         * The login name is used for looking up the account.
096         *
097         * @param username The name of the user to be checked.
098         * @return true if the specified account exists
099         * @throws DataBackendException Error accessing the data backend.
100         */
101        public boolean accountExists(String username)
102                throws DataBackendException
103        {
104            try
105            {
106                User ldapUser = retrieve(username);
107            }
108            catch (UnknownEntityException ex)
109            {
110                return false;
111            }
112    
113            return true;
114        }
115    
116        /**
117         * Retrieve a user from persistent storage using username as the
118         * key.
119         *
120         * @param username the name of the user.
121         * @return an User object.
122         * @exception UnknownEntityException if the user's account does not
123         *            exist in the database.
124         * @exception DataBackendException Error accessing the data backend.
125         */
126        public User retrieve(String username)
127                throws UnknownEntityException, DataBackendException
128        {
129            try
130            {
131                DirContext ctx = bindAsAdmin();
132    
133                /*
134                 * Define the search.
135                 */
136                String userBaseSearch = LDAPSecurityConstants.getBaseSearch();
137                String filter = LDAPSecurityConstants.getNameAttribute();
138    
139                filter = "(" + filter + "=" + username + ")";
140    
141                /*
142                 * Create the default search controls.
143                 */
144                SearchControls ctls = new SearchControls();
145    
146                NamingEnumeration answer =
147                        ctx.search(userBaseSearch, filter, ctls);
148    
149                if (answer.hasMore())
150                {
151                    SearchResult sr = (SearchResult) answer.next();
152                    Attributes attribs = sr.getAttributes();
153                    LDAPUser ldapUser = createLDAPUser();
154    
155                    ldapUser.setLDAPAttributes(attribs);
156                    ldapUser.setTemp("turbine.user", ldapUser);
157    
158                    return ldapUser;
159                }
160                else
161                {
162                    throw new UnknownEntityException("The given user: "
163                            + username + "\n does not exist.");
164                }
165            }
166            catch (NamingException ex)
167            {
168                throw new DataBackendException(
169                        "The LDAP server specified is unavailable", ex);
170            }
171        }
172    
173        /**
174         * This is currently not implemented to behave as expected.  It
175         * ignores the Criteria argument and returns all the users.
176         *
177         * Retrieve a set of users that meet the specified criteria.
178         *
179         * As the keys for the criteria, you should use the constants that
180         * are defined in {@link User} interface, plus the the names
181         * of the custom attributes you added to your user representation
182         * in the data storage. Use verbatim names of the attributes -
183         * without table name prefix in case of DB implementation.
184         *
185         * @param criteria The criteria of selection.
186         * @return a List of users meeting the criteria.
187         * @throws DataBackendException Error accessing the data backend.
188         * @deprecated Use <a href="#retrieveList">retrieveList</a> instead.
189         */
190        public User[] retrieve(Object criteria)
191                throws DataBackendException
192        {
193            return (User []) retrieveList(criteria).toArray(new User[0]);
194        }
195    
196        /**
197         * Retrieve a list of users that meet the specified criteria.
198         *
199         * As the keys for the criteria, you should use the constants that
200         * are defined in {@link User} interface, plus the names
201         * of the custom attributes you added to your user representation
202         * in the data storage. Use verbatim names of the attributes -
203         * without table name prefix in case of Torque implementation.
204         *
205         * @param criteria The criteria of selection.
206         * @return a List of users meeting the criteria.
207         * @throws DataBackendException if there is a problem accessing the
208         *         storage.
209         */
210        public List retrieveList(Object criteria)
211                throws DataBackendException
212        {
213            List users = new Vector(0);
214    
215            try
216            {
217                DirContext ctx = bindAsAdmin();
218    
219                String userBaseSearch = LDAPSecurityConstants.getBaseSearch();
220                String filter = LDAPSecurityConstants.getNameAttribute();
221    
222                filter = "(" + filter + "=*)";
223    
224                /*
225                 * Create the default search controls.
226                 */
227                SearchControls ctls = new SearchControls();
228    
229                NamingEnumeration answer =
230                        ctx.search(userBaseSearch, filter, ctls);
231    
232                while (answer.hasMore())
233                {
234                    SearchResult sr = (SearchResult) answer.next();
235                    Attributes attribs = sr.getAttributes();
236                    LDAPUser ldapUser = createLDAPUser();
237    
238                    ldapUser.setLDAPAttributes(attribs);
239                    ldapUser.setTemp("turbine.user", ldapUser);
240                    users.add(ldapUser);
241                }
242            }
243            catch (NamingException ex)
244            {
245                throw new DataBackendException(
246                        "The LDAP server specified is unavailable", ex);
247            }
248            return users;
249        }
250    
251        /**
252         * Retrieve a user from persistent storage using username as the
253         * key, and authenticate the user. The implementation may chose
254         * to authenticate to the server as the user whose data is being
255         * retrieved.
256         *
257         * @param username the name of the user.
258         * @param password the user supplied password.
259         * @return an User object.
260         * @exception PasswordMismatchException if the supplied password was
261         *            incorrect.
262         * @exception UnknownEntityException if the user's account does not
263         *            exist in the database.
264         * @exception DataBackendException Error accessing the data backend.
265         */
266        public User retrieve(String username, String password)
267                throws PasswordMismatchException,
268                UnknownEntityException, DataBackendException
269        {
270            User user = retrieve(username);
271    
272            authenticate(user, password);
273            return user;
274        }
275    
276        /**
277         * Save a User object to persistent storage. User's account is
278         * required to exist in the storage.
279         *
280         * @param user an User object to store.
281         * @throws UnknownEntityException if the user's account does not
282         *            exist in the database.
283         * @throws DataBackendException if there is an LDAP error
284         *
285         */
286        public void store(User user)
287                throws UnknownEntityException, DataBackendException
288        {
289            if (!accountExists(user))
290            {
291                throw new UnknownEntityException("The account '"
292                        + user.getName() + "' does not exist");
293            }
294    
295            try
296            {
297                LDAPUser ldapUser = (LDAPUser) user;
298                Attributes attrs = ldapUser.getLDAPAttributes();
299                String name = ldapUser.getDN();
300    
301                DirContext ctx = bindAsAdmin();
302    
303                ctx.modifyAttributes(name, DirContext.REPLACE_ATTRIBUTE, attrs);
304            }
305            catch (NamingException ex)
306            {
307                throw new DataBackendException("NamingException caught", ex);
308            }
309        }
310    
311        /**
312         * This method is not yet implemented.
313         * Saves User data when the session is unbound. The user account is required
314         * to exist in the storage.
315         *
316         * LastLogin, AccessCounter, persistent pull tools, and any data stored
317         * in the permData hashtable that is not mapped to a column will be saved.
318         *
319         * @exception UnknownEntityException if the user's account does not
320         *            exist in the database.
321         * @exception DataBackendException if there is a problem accessing the
322         *            storage.
323         */
324        public void saveOnSessionUnbind(User user)
325                throws UnknownEntityException, DataBackendException
326        {
327            if (!accountExists(user))
328            {
329                throw new UnknownEntityException("The account '" +
330                        user.getName() + "' does not exist");
331            }
332        }
333    
334        /**
335         * Authenticate a User with the specified password. If authentication
336         * is successful the method returns nothing. If there are any problems,
337         * exception was thrown.
338         *
339         * @param user a User object to authenticate.
340         * @param password the user supplied password.
341         * @exception PasswordMismatchException if the supplied password was
342         *            incorrect.
343         * @exception UnknownEntityException if the user's account does not
344         *            exist in the database.
345         * @exception DataBackendException Error accessing the data backend.
346         */
347        public void authenticate(User user, String password)
348                throws PasswordMismatchException,
349                UnknownEntityException,
350                DataBackendException
351        {
352            LDAPUser ldapUser = (LDAPUser) user;
353    
354            try
355            {
356                bind(ldapUser.getDN(), password);
357            }
358            catch (AuthenticationException ex)
359            {
360                throw new PasswordMismatchException(
361                        "The given password for: "
362                        + ldapUser.getDN() + " is invalid\n");
363            }
364            catch (NamingException ex)
365            {
366                throw new DataBackendException(
367                        "NamingException caught:", ex);
368            }
369        }
370    
371        /**
372         * This method is not yet implemented
373         * Change the password for an User.
374         *
375         * @param user an User to change password for.
376         * @param newPass the new password.
377         * @param oldPass the old password.
378         * @exception PasswordMismatchException if the supplied password was
379         *            incorrect.
380         * @exception UnknownEntityException if the user's account does not
381         *            exist in the database.
382         * @exception DataBackendException Error accessing the data backend.
383         */
384        public void changePassword(User user, String oldPass, String newPass)
385                throws PasswordMismatchException,
386                UnknownEntityException, DataBackendException
387        {
388            throw new DataBackendException(
389                    "The method changePassword has no implementation.");
390        }
391    
392        /**
393         * This method is not yet implemented
394         * Forcibly sets new password for an User.
395         *
396         * This is supposed to be used by the administrator to change the forgotten
397         * or compromised passwords. Certain implementatations of this feature
398         * would require adminstrative level access to the authenticating
399         * server / program.
400         *
401         * @param user an User to change password for.
402         * @param password the new password.
403         * @exception UnknownEntityException if the user's record does not
404         *            exist in the database.
405         * @exception DataBackendException Error accessing the data backend.
406         */
407        public void forcePassword(User user, String password)
408                throws UnknownEntityException, DataBackendException
409        {
410            throw new DataBackendException(
411                    "The method forcePassword has no implementation.");
412        }
413    
414        /**
415         * Creates new user account with specified attributes.
416         *
417         * @param user the object describing account to be created.
418         * @param initialPassword Not used yet.
419         * @throws DataBackendException Error accessing the data backend.
420         * @throws EntityExistsException if the user account already exists.
421         */
422        public void createAccount(User user, String initialPassword)
423                throws EntityExistsException, DataBackendException
424        {
425            if (accountExists(user))
426            {
427                throw new EntityExistsException("The account '"
428                        + user.getName() + "' already exist");
429            }
430    
431            try
432            {
433                LDAPUser ldapUser = (LDAPUser) user;
434                Attributes attrs = ldapUser.getLDAPAttributes();
435                String name = ldapUser.getDN();
436    
437                DirContext ctx = bindAsAdmin();
438    
439                ctx.bind(name, null, attrs);
440            }
441            catch (NamingException ex)
442            {
443                throw new DataBackendException("NamingException caught", ex);
444            }
445        }
446    
447        /**
448         * Removes an user account from the system.
449         *
450         * @param user the object describing the account to be removed.
451         * @throws DataBackendException Error accessing the data backend.
452         * @throws UnknownEntityException if the user account is not present.
453         */
454        public void removeAccount(User user)
455                throws UnknownEntityException, DataBackendException
456        {
457            if (!accountExists(user))
458            {
459                throw new UnknownEntityException("The account '"
460                        + user.getName() + "' does not exist");
461            }
462    
463            try
464            {
465                LDAPUser ldapUser = (LDAPUser) user;
466                String name = ldapUser.getDN();
467    
468                DirContext ctx = bindAsAdmin();
469    
470                ctx.unbind(name);
471            }
472            catch (NamingException ex)
473            {
474                throw new DataBackendException("NamingException caught", ex);
475            }
476        }
477    
478        /**
479         * Bind as the admin user.
480         *
481         * @throws NamingException when an error occurs with the named server.
482         * @return a new DirContext.
483         */
484        public static DirContext bindAsAdmin()
485                throws NamingException
486        {
487            String adminUser = LDAPSecurityConstants.getAdminUsername();
488            String adminPassword = LDAPSecurityConstants.getAdminPassword();
489    
490            return bind(adminUser, adminPassword);
491        }
492    
493        /**
494         * Creates an initial context.
495         *
496         * @param username admin username supplied in TRP.
497         * @param password admin password supplied in TRP
498         * @throws NamingException when an error occurs with the named server.
499         * @return a new DirContext.
500         */
501        public static DirContext bind(String username, String password)
502                throws NamingException
503        {
504            String host = LDAPSecurityConstants.getLDAPHost();
505            String port = LDAPSecurityConstants.getLDAPPort();
506            String providerURL = "ldap://" + host + ":" + port;
507            String ldapProvider = LDAPSecurityConstants.getLDAPProvider();
508            String authentication = LDAPSecurityConstants.getLDAPAuthentication();
509    
510            /*
511             * creating an initial context using Sun's client
512             * LDAP Provider.
513             */
514            Hashtable env = new Hashtable();
515    
516            env.put(Context.INITIAL_CONTEXT_FACTORY, ldapProvider);
517            env.put(Context.PROVIDER_URL, providerURL);
518            env.put(Context.SECURITY_AUTHENTICATION, authentication);
519            env.put(Context.SECURITY_PRINCIPAL, username);
520            env.put(Context.SECURITY_CREDENTIALS, password);
521    
522            DirContext ctx = new javax.naming.directory.InitialDirContext(env);
523    
524            return ctx;
525        }
526    
527        /**
528         * Create a new instance of the LDAP User according to the value
529         * configured in TurbineResources.properties.
530         * @return a new instance of the LDAP User.
531         * @throws DataBackendException if there is an error creating the
532         */
533        private LDAPUser createLDAPUser()
534                throws DataBackendException
535        {
536            try
537            {
538                return (LDAPUser) TurbineSecurity.getUserInstance();
539            }
540            catch (ClassCastException ex)
541            {
542                throw new DataBackendException("ClassCastException:", ex);
543            }
544            catch (UnknownEntityException ex)
545            {
546                throw new DataBackendException("UnknownEntityException:", ex);
547            }
548        }
549    
550    }