0001: /*
0002: * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/realm/JNDIRealm.java,v 1.8 2002/06/11 15:32:28 remm Exp $
0003: * $Revision: 1.8 $
0004: * $Date: 2002/06/11 15:32:28 $
0005: *
0006: * ====================================================================
0007: * The Apache Software License, Version 1.1
0008: *
0009: * Copyright (c) 1999-2002 The Apache Software Foundation. All rights
0010: * reserved.
0011: *
0012: * Redistribution and use in source and binary forms, with or without
0013: * modification, are permitted provided that the following conditions
0014: * are met:
0015: *
0016: * 1. Redistributions of source code must retain the above copyright
0017: * notice, this list of conditions and the following disclaimer.
0018: *
0019: * 2. Redistributions in binary form must reproduce the above copyright
0020: * notice, this list of conditions and the following disclaimer in
0021: * the documentation and/or other materials provided with the
0022: * distribution.
0023: *
0024: * 3. The end-user documentation included with the redistribution, if
0025: * any, must include the following acknowlegement:
0026: * "This product includes software developed by the
0027: * Apache Software Foundation (http://www.apache.org/)."
0028: * Alternately, this acknowlegement may appear in the software itself,
0029: * if and wherever such third-party acknowlegements normally appear.
0030: *
0031: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
0032: * Foundation" must not be used to endorse or promote products derived
0033: * from this software without prior written permission. For written
0034: * permission, please contact apache@apache.org.
0035: *
0036: * 5. Products derived from this software may not be called "Apache"
0037: * nor may "Apache" appear in their names without prior written
0038: * permission of the Apache Group.
0039: *
0040: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
0041: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
0042: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
0043: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
0044: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
0045: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
0046: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
0047: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
0048: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
0049: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
0050: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
0051: * SUCH DAMAGE.
0052: * ====================================================================
0053: *
0054: * This software consists of voluntary contributions made by many
0055: * individuals on behalf of the Apache Software Foundation. For more
0056: * information on the Apache Software Foundation, please see
0057: * <http://www.apache.org/>.
0058: *
0059: * [Additional notices, if required by prior licensing conditions]
0060: *
0061: */
0062:
0063: package org.apache.catalina.realm;
0064:
0065: import java.security.Principal;
0066: import java.text.MessageFormat;
0067: import java.util.ArrayList;
0068: import java.util.Hashtable;
0069: import java.util.List;
0070: import javax.naming.Context;
0071: import javax.naming.NameNotFoundException;
0072: import javax.naming.NamingEnumeration;
0073: import javax.naming.NamingException;
0074: import javax.naming.NameParser;
0075: import javax.naming.Name;
0076: import javax.naming.AuthenticationException;
0077: import javax.naming.directory.Attribute;
0078: import javax.naming.directory.Attributes;
0079: import javax.naming.directory.DirContext;
0080: import javax.naming.directory.InitialDirContext;
0081: import javax.naming.directory.SearchControls;
0082: import javax.naming.directory.SearchResult;
0083: import org.apache.catalina.LifecycleException;
0084: import org.apache.catalina.Realm;
0085: import org.apache.catalina.util.StringManager;
0086:
0087: /**
0088: * <p>Implementation of <strong>Realm</strong> that works with a directory
0089: * server accessed via the Java Naming and Directory Interface (JNDI) APIs.
0090: * The following constraints are imposed on the data structure in the
0091: * underlying directory server:</p>
0092: * <ul>
0093: *
0094: * <li>Each user that can be authenticated is represented by an individual
0095: * element in the top level <code>DirContext</code> that is accessed
0096: * via the <code>connectionURL</code> property.</li>
0097: *
0098: * <li>Each user element has a distinguished name that can be formed by
0099: * substituting the presented username into a pattern configured by the
0100: * <code>userPattern</code> property.</li>
0101: *
0102: * <li>Alternatively, if the <code>userPattern</code> property is not
0103: * specified, a unique element can be located by searching the directory
0104: * context. In this case:
0105: * <ul>
0106: * <li>The <code>userSearch</code> pattern specifies the search filter
0107: * after substitution of the username.</li>
0108: * <li>The <code>userBase</code> property can be set to the element that
0109: * is the base of the subtree containing users. If not specified,
0110: * the search base is the top-level context.</li>
0111: * <li>The <code>userSubtree</code> property can be set to
0112: * <code>true</code> if you wish to search the entire subtree of the
0113: * directory context. The default value of <code>false</code>
0114: * requests a search of only the current level.</li>
0115: * </ul>
0116: * </li>
0117: *
0118: * <li>The user may be authenticated by binding to the directory with the
0119: * username and password presented. This method is used when the
0120: * <code>userPassword</code> property is not specified.</li>
0121: *
0122: * <li>The user may be authenticated by retrieving the value of an attribute
0123: * from the directory and comparing it explicitly with the value presented
0124: * by the user. This method is used when the <code>userPassword</code>
0125: * property is specified, in which case:
0126: * <ul>
0127: * <li>The element for this user must contain an attribute named by the
0128: * <code>userPassword</code> property.
0129: * <li>The value of the user password attribute is either a cleartext
0130: * String, or the result of passing a cleartext String through the
0131: * <code>RealmBase.digest()</code> method (using the standard digest
0132: * support included in <code>RealmBase</code>).
0133: * <li>The user is considered to be authenticated if the presented
0134: * credentials (after being passed through
0135: * <code>RealmBase.digest()</code>) are equal to the retrieved value
0136: * for the user password attribute.</li>
0137: * </ul></li>
0138: *
0139: * <li>Each group of users that has been assigned a particular role may be
0140: * represented by an individual element in the top level
0141: * <code>DirContext</code> that is accessed via the
0142: * <code>connectionURL</code> property. This element has the following
0143: * characteristics:
0144: * <ul>
0145: * <li>The set of all possible groups of interest can be selected by a
0146: * search pattern configured by the <code>roleSearch</code>
0147: * property.</li>
0148: * <li>The <code>roleSearch</code> pattern optionally includes pattern
0149: * replacements "{0}" for the distinguished name, and/or "{1}" for
0150: * the username, of the authenticated user for which roles will be
0151: * retrieved.</li>
0152: * <li>The <code>roleBase</code> property can be set to the element that
0153: * is the base of the search for matching roles. If not specified,
0154: * the entire context will be searched.</li>
0155: * <li>The <code>roleSubtree</code> property can be set to
0156: * <code>true</code> if you wish to search the entire subtree of the
0157: * directory context. The default value of <code>false</code>
0158: * requests a search of only the current level.</li>
0159: * <li>The element includes an attribute (whose name is configured by
0160: * the <code>roleName</code> property) containing the name of the
0161: * role represented by this element.</li>
0162: * </ul></li>
0163: *
0164: * <li>In addition, roles may be represented by the values of an attribute
0165: * in the user's element whose name is configured by the
0166: * <code>userRoleName</code> property.</li>
0167: *
0168: * <li>Note that the standard <code><security-role-ref></code> element in
0169: * the web application deployment descriptor allows applications to refer
0170: * to roles programmatically by names other than those used in the
0171: * directory server itself.</li>
0172: * </ul>
0173: *
0174: * <p><strong>TODO</strong> - Support connection pooling (including message
0175: * format objects) so that <code>authenticate()</code> does not have to be
0176: * synchronized.</p>
0177: *
0178: * @author John Holman
0179: * @author Craig R. McClanahan
0180: * @version $Revision: 1.8 $ $Date: 2002/06/11 15:32:28 $
0181: */
0182:
0183: public class JNDIRealm extends RealmBase {
0184:
0185: // ----------------------------------------------------- Instance Variables
0186:
0187: /**
0188: * The connection username for the server we will contact.
0189: */
0190: protected String connectionName = null;
0191:
0192: /**
0193: * The connection password for the server we will contact.
0194: */
0195: protected String connectionPassword = null;
0196:
0197: /**
0198: * The connection URL for the server we will contact.
0199: */
0200: protected String connectionURL = null;
0201:
0202: /**
0203: * The directory context linking us to our directory server.
0204: */
0205: protected DirContext context = null;
0206:
0207: /**
0208: * The JNDI context factory used to acquire our InitialContext. By
0209: * default, assumes use of an LDAP server using the standard JNDI LDAP
0210: * provider.
0211: */
0212: protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
0213:
0214: /**
0215: * Descriptive information about this Realm implementation.
0216: */
0217: protected static final String info = "org.apache.catalina.realm.JNDIRealm/1.0";
0218:
0219: /**
0220: * Descriptive information about this Realm implementation.
0221: */
0222: protected static final String name = "JNDIRealm";
0223:
0224: /**
0225: * The base element for user searches.
0226: */
0227: protected String userBase = "";
0228:
0229: /**
0230: * The message format used to search for a user, with "{0}" marking
0231: * the spot where the username goes.
0232: */
0233: protected String userSearch = null;
0234:
0235: /**
0236: * The MessageFormat object associated with the current
0237: * <code>userSearch</code>.
0238: */
0239: protected MessageFormat userSearchFormat = null;
0240:
0241: /**
0242: * Should we search the entire subtree for matching users?
0243: */
0244: protected boolean userSubtree = false;
0245:
0246: /**
0247: * The attribute name used to retrieve the user password.
0248: */
0249: protected String userPassword = null;
0250:
0251: /**
0252: * The message format used to form the distinguished name of a
0253: * user, with "{0}" marking the spot where the specified username
0254: * goes.
0255: */
0256: protected String userPattern = null;
0257:
0258: /**
0259: * The MessageFormat object associated with the current
0260: * <code>userPattern</code>.
0261: */
0262: protected MessageFormat userPatternFormat = null;
0263:
0264: /**
0265: * The base element for role searches.
0266: */
0267: protected String roleBase = "";
0268:
0269: /**
0270: * The MessageFormat object associated with the current
0271: * <code>roleSearch</code>.
0272: */
0273: protected MessageFormat roleFormat = null;
0274:
0275: /**
0276: * The name of an attribute in the user's entry containing
0277: * roles for that user
0278: */
0279: protected String userRoleName = null;
0280:
0281: /**
0282: * The name of the attribute containing roles held elsewhere
0283: */
0284: protected String roleName = null;
0285:
0286: /**
0287: * The message format used to select roles for a user, with "{0}" marking
0288: * the spot where the distinguished name of the user goes.
0289: */
0290: protected String roleSearch = null;
0291:
0292: /**
0293: * Should we search the entire subtree for matching memberships?
0294: */
0295: protected boolean roleSubtree = false;
0296:
0297: // ------------------------------------------------------------- Properties
0298:
0299: /**
0300: * Return the connection username for this Realm.
0301: */
0302: public String getConnectionName() {
0303:
0304: return (this .connectionName);
0305:
0306: }
0307:
0308: /**
0309: * Set the connection username for this Realm.
0310: *
0311: * @param connectionName The new connection username
0312: */
0313: public void setConnectionName(String connectionName) {
0314:
0315: this .connectionName = connectionName;
0316:
0317: }
0318:
0319: /**
0320: * Return the connection password for this Realm.
0321: */
0322: public String getConnectionPassword() {
0323:
0324: return (this .connectionPassword);
0325:
0326: }
0327:
0328: /**
0329: * Set the connection password for this Realm.
0330: *
0331: * @param connectionPassword The new connection password
0332: */
0333: public void setConnectionPassword(String connectionPassword) {
0334:
0335: this .connectionPassword = connectionPassword;
0336:
0337: }
0338:
0339: /**
0340: * Return the connection URL for this Realm.
0341: */
0342: public String getConnectionURL() {
0343:
0344: return (this .connectionURL);
0345:
0346: }
0347:
0348: /**
0349: * Set the connection URL for this Realm.
0350: *
0351: * @param connectionURL The new connection URL
0352: */
0353: public void setConnectionURL(String connectionURL) {
0354:
0355: this .connectionURL = connectionURL;
0356:
0357: }
0358:
0359: /**
0360: * Return the JNDI context factory for this Realm.
0361: */
0362: public String getContextFactory() {
0363:
0364: return (this .contextFactory);
0365:
0366: }
0367:
0368: /**
0369: * Set the JNDI context factory for this Realm.
0370: *
0371: * @param contextFactory The new context factory
0372: */
0373: public void setContextFactory(String contextFactory) {
0374:
0375: this .contextFactory = contextFactory;
0376:
0377: }
0378:
0379: /**
0380: * Return the base element for user searches.
0381: */
0382: public String getUserBase() {
0383:
0384: return (this .userBase);
0385:
0386: }
0387:
0388: /**
0389: * Set the base element for user searches.
0390: *
0391: * @param userBase The new base element
0392: */
0393: public void setUserBase(String userBase) {
0394:
0395: this .userBase = userBase;
0396:
0397: }
0398:
0399: /**
0400: * Return the message format pattern for selecting users in this Realm.
0401: */
0402: public String getUserSearch() {
0403:
0404: return (this .userSearch);
0405:
0406: }
0407:
0408: /**
0409: * Set the message format pattern for selecting users in this Realm.
0410: *
0411: * @param userSearch The new user search pattern
0412: */
0413: public void setUserSearch(String userSearch) {
0414:
0415: this .userSearch = userSearch;
0416: if (userSearch == null)
0417: userSearchFormat = null;
0418: else
0419: userSearchFormat = new MessageFormat(userSearch);
0420:
0421: }
0422:
0423: /**
0424: * Return the "search subtree for users" flag.
0425: */
0426: public boolean getUserSubtree() {
0427:
0428: return (this .userSubtree);
0429:
0430: }
0431:
0432: /**
0433: * Set the "search subtree for users" flag.
0434: *
0435: * @param userSubtree The new search flag
0436: */
0437: public void setUserSubtree(boolean userSubtree) {
0438:
0439: this .userSubtree = userSubtree;
0440:
0441: }
0442:
0443: /**
0444: * Return the user role name attribute name for this Realm.
0445: */
0446: public String getUserRoleName() {
0447:
0448: return userRoleName;
0449: }
0450:
0451: /**
0452: * Set the user role name attribute name for this Realm.
0453: *
0454: * @param userRoleName The new userRole name attribute name
0455: */
0456: public void setUserRoleName(String userRoleName) {
0457:
0458: this .userRoleName = userRoleName;
0459:
0460: }
0461:
0462: /**
0463: * Return the base element for role searches.
0464: */
0465: public String getRoleBase() {
0466:
0467: return (this .roleBase);
0468:
0469: }
0470:
0471: /**
0472: * Set the base element for role searches.
0473: *
0474: * @param roleBase The new base element
0475: */
0476: public void setRoleBase(String roleBase) {
0477:
0478: this .roleBase = roleBase;
0479:
0480: }
0481:
0482: /**
0483: * Return the role name attribute name for this Realm.
0484: */
0485: public String getRoleName() {
0486:
0487: return (this .roleName);
0488:
0489: }
0490:
0491: /**
0492: * Set the role name attribute name for this Realm.
0493: *
0494: * @param roleName The new role name attribute name
0495: */
0496: public void setRoleName(String roleName) {
0497:
0498: this .roleName = roleName;
0499:
0500: }
0501:
0502: /**
0503: * Return the message format pattern for selecting roles in this Realm.
0504: */
0505: public String getRoleSearch() {
0506:
0507: return (this .roleSearch);
0508:
0509: }
0510:
0511: /**
0512: * Set the message format pattern for selecting roles in this Realm.
0513: *
0514: * @param roleSearch The new role search pattern
0515: */
0516: public void setRoleSearch(String roleSearch) {
0517:
0518: this .roleSearch = roleSearch;
0519: if (roleSearch == null)
0520: roleFormat = null;
0521: else
0522: roleFormat = new MessageFormat(roleSearch);
0523:
0524: }
0525:
0526: /**
0527: * Return the "search subtree for roles" flag.
0528: */
0529: public boolean getRoleSubtree() {
0530:
0531: return (this .roleSubtree);
0532:
0533: }
0534:
0535: /**
0536: * Set the "search subtree for roles" flag.
0537: *
0538: * @param roleSubtree The new search flag
0539: */
0540: public void setRoleSubtree(boolean roleSubtree) {
0541:
0542: this .roleSubtree = roleSubtree;
0543:
0544: }
0545:
0546: /**
0547: * Return the password attribute used to retrieve the user password.
0548: */
0549: public String getUserPassword() {
0550:
0551: return (this .userPassword);
0552:
0553: }
0554:
0555: /**
0556: * Set the password attribute used to retrieve the user password.
0557: *
0558: * @param userPassword The new password attribute
0559: */
0560: public void setUserPassword(String userPassword) {
0561:
0562: this .userPassword = userPassword;
0563:
0564: }
0565:
0566: /**
0567: * Return the message format pattern for selecting users in this Realm.
0568: */
0569: public String getUserPattern() {
0570:
0571: return (this .userPattern);
0572:
0573: }
0574:
0575: /**
0576: * Set the message format pattern for selecting users in this Realm.
0577: *
0578: * @param userPattern The new user pattern
0579: */
0580: public void setUserPattern(String userPattern) {
0581:
0582: this .userPattern = userPattern;
0583: if (userPattern == null)
0584: userPatternFormat = null;
0585: else
0586: userPatternFormat = new MessageFormat(userPattern);
0587:
0588: }
0589:
0590: // ---------------------------------------------------------- Realm Methods
0591:
0592: /**
0593: * Return the Principal associated with the specified username and
0594: * credentials, if there is one; otherwise return <code>null</code>.
0595: *
0596: * If there are any errors with the JDBC connection, executing
0597: * the query or anything we return null (don't authenticate). This
0598: * event is also logged, and the connection will be closed so that
0599: * a subsequent request will automatically re-open it.
0600: *
0601: * @param username Username of the Principal to look up
0602: * @param credentials Password or other credentials to use in
0603: * authenticating this username
0604: */
0605: public Principal authenticate(String username, String credentials) {
0606:
0607: DirContext context = null;
0608:
0609: try {
0610:
0611: // Ensure that we have a directory context available
0612: context = open();
0613:
0614: // Authenticate the specified username if possible
0615: Principal principal = authenticate(context, username,
0616: credentials);
0617:
0618: // Release this context
0619: release(context);
0620:
0621: // Return the authenticated Principal (if any)
0622: return (principal);
0623:
0624: } catch (NamingException e) {
0625:
0626: // Log the problem for posterity
0627: log(sm.getString("jndiRealm.exception"), e);
0628:
0629: // Close the connection so that it gets reopened next time
0630: if (context != null)
0631: close(context);
0632:
0633: // Return "not authenticated" for this request
0634: return (null);
0635:
0636: }
0637:
0638: }
0639:
0640: // -------------------------------------------------------- Package Methods
0641:
0642: // ------------------------------------------------------ Protected Methods
0643:
0644: /**
0645: * Return the Principal associated with the specified username and
0646: * credentials, if there is one; otherwise return <code>null</code>.
0647: *
0648: * @param context The directory context
0649: * @param username Username of the Principal to look up
0650: * @param credentials Password or other credentials to use in
0651: * authenticating this username
0652: *
0653: * @exception NamingException if a directory server error occurs
0654: */
0655: public synchronized Principal authenticate(DirContext context,
0656: String username, String credentials) throws NamingException {
0657:
0658: if (username == null || username.equals("")
0659: || credentials == null || credentials.equals(""))
0660: return (null);
0661:
0662: // Retrieve user information
0663: User user = getUser(context, username);
0664: if (user == null)
0665: return (null);
0666:
0667: // Check the user's credentials
0668: if (!checkCredentials(context, user, credentials))
0669: return (null);
0670:
0671: // Search for additional roles
0672: List roles = getRoles(context, user);
0673:
0674: // Create and return a suitable Principal for this user
0675: return (new GenericPrincipal(this , username, credentials, roles));
0676:
0677: }
0678:
0679: /**
0680: * Return a User object containing information about the user
0681: * with the specified username, if found in the directory;
0682: * otherwise return <code>null</code>.
0683: *
0684: * If the <code>userPassword</code> configuration attribute is
0685: * specified, the value of that attribute is retrieved from the
0686: * user's directory entry. If the <code>userRoleName</code>
0687: * configuration attribute is specified, all values of that
0688: * attribute are retrieved from the directory entry.
0689: *
0690: * @param context The directory context
0691: * @param username Username to be looked up
0692: *
0693: * @exception NamingException if a directory server error occurs
0694: */
0695: protected User getUser(DirContext context, String username)
0696: throws NamingException {
0697:
0698: User user = null;
0699:
0700: // Get attributes to retrieve from user entry
0701: ArrayList list = new ArrayList();
0702: if (userPassword != null)
0703: list.add(userPassword);
0704: if (userRoleName != null)
0705: list.add(userRoleName);
0706: String[] attrIds = new String[list.size()];
0707: list.toArray(attrIds);
0708:
0709: // Use pattern or search for user entry
0710: if (userPatternFormat != null) {
0711: user = getUserByPattern(context, username, attrIds);
0712: } else {
0713: user = getUserBySearch(context, username, attrIds);
0714: }
0715:
0716: return user;
0717: }
0718:
0719: /**
0720: * Use the <code>UserPattern</code> configuration attribute to
0721: * locate the directory entry for the user with the specified
0722: * username and return a User object; otherwise return
0723: * <code>null</code>.
0724: *
0725: * @param context The directory context
0726: * @param username The username
0727: * @param attrIds String[]containing names of attributes to
0728: * retrieve.
0729: *
0730: * @exception NamingException if a directory server error occurs
0731: */
0732: protected User getUserByPattern(DirContext context,
0733: String username, String[] attrIds) throws NamingException {
0734:
0735: if (debug >= 2)
0736: log("lookupUser(" + username + ")");
0737:
0738: if (username == null || userPatternFormat == null)
0739: return (null);
0740:
0741: // Form the dn from the user pattern
0742: String dn = userPatternFormat.format(new String[] { username });
0743: if (debug >= 3) {
0744: log(" dn=" + dn);
0745: }
0746:
0747: // Return if no attributes to retrieve
0748: if (attrIds == null || attrIds.length == 0)
0749: return new User(username, dn, null, null);
0750:
0751: // Get required attributes from user entry
0752: Attributes attrs = null;
0753: try {
0754: attrs = context.getAttributes(dn, attrIds);
0755: } catch (NameNotFoundException e) {
0756: return (null);
0757: }
0758: if (attrs == null)
0759: return (null);
0760:
0761: // Retrieve value of userPassword
0762: String password = null;
0763: if (userPassword != null)
0764: password = getAttributeValue(userPassword, attrs);
0765:
0766: // Retrieve values of userRoleName attribute
0767: ArrayList roles = null;
0768: if (userRoleName != null)
0769: roles = addAttributeValues(userRoleName, attrs, roles);
0770:
0771: return new User(username, dn, password, roles);
0772: }
0773:
0774: /**
0775: * Search the directory to return a User object containing
0776: * information about the user with the specified username, if
0777: * found in the directory; otherwise return <code>null</code>.
0778: *
0779: * @param context The directory context
0780: * @param username The username
0781: * @param attrIds String[]containing names of attributes to retrieve.
0782: *
0783: * @exception NamingException if a directory server error occurs
0784: */
0785: protected User getUserBySearch(DirContext context, String username,
0786: String[] attrIds) throws NamingException {
0787:
0788: if (username == null || userSearchFormat == null)
0789: return (null);
0790:
0791: // Form the search filter
0792: String filter = userSearchFormat
0793: .format(new String[] { username });
0794:
0795: // Set up the search controls
0796: SearchControls constraints = new SearchControls();
0797:
0798: if (userSubtree) {
0799: constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
0800: } else {
0801: constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
0802: }
0803:
0804: // Specify the attributes to be retrieved
0805: if (attrIds == null)
0806: attrIds = new String[0];
0807: constraints.setReturningAttributes(attrIds);
0808:
0809: if (debug > 3) {
0810: log(" Searching for " + username);
0811: log(" base: " + userBase + " filter: " + filter);
0812: }
0813:
0814: NamingEnumeration results = context.search(userBase, filter,
0815: constraints);
0816:
0817: // Fail if no entries found
0818: if (results == null || !results.hasMore()) {
0819: if (debug > 2) {
0820: log(" username not found");
0821: }
0822: return (null);
0823: }
0824:
0825: // Get result for the first entry found
0826: SearchResult result = (SearchResult) results.next();
0827:
0828: // Check no further entries were found
0829: if (results.hasMore()) {
0830: log("username " + username + " has multiple entries");
0831: return (null);
0832: }
0833:
0834: // Get the entry's distinguished name
0835: NameParser parser = context.getNameParser("");
0836: Name contextName = parser.parse(context.getNameInNamespace());
0837: Name baseName = parser.parse(userBase);
0838: Name entryName = parser.parse(result.getName());
0839: Name name = contextName.addAll(baseName);
0840: name = name.addAll(entryName);
0841: String dn = name.toString();
0842:
0843: if (debug > 2)
0844: log(" entry found for " + username + " with dn " + dn);
0845:
0846: // Get the entry's attributes
0847: Attributes attrs = result.getAttributes();
0848: if (attrs == null)
0849: return null;
0850:
0851: // Retrieve value of userPassword
0852: String password = null;
0853: if (userPassword != null)
0854: password = getAttributeValue(userPassword, attrs);
0855:
0856: // Retrieve values of userRoleName attribute
0857: ArrayList roles = null;
0858: if (userRoleName != null)
0859: roles = addAttributeValues(userRoleName, attrs, roles);
0860:
0861: return new User(username, dn, password, roles);
0862: }
0863:
0864: /**
0865: * Check whether the given User can be authenticated with the
0866: * given credentials. If the <code>userPassword</code>
0867: * configuration attribute is specified, the credentials
0868: * previously retrieved from the directory are compared explicitly
0869: * with those presented by the user. Otherwise the presented
0870: * credentials are checked by binding to the directory as the
0871: * user.
0872: *
0873: * @param context The directory context
0874: * @param user The User to be authenticated
0875: * @param credentials The credentials presented by the user
0876: *
0877: * @exception NamingException if a directory server error occurs
0878: */
0879: protected boolean checkCredentials(DirContext context, User user,
0880: String credentials) throws NamingException {
0881:
0882: boolean validated = false;
0883:
0884: if (userPassword == null) {
0885: validated = bindAsUser(context, user, credentials);
0886: } else {
0887: validated = compareCredentials(context, user, credentials);
0888: }
0889:
0890: if (debug >= 2) {
0891: if (validated) {
0892: log(sm.getString("jndiRealm.authenticateSuccess",
0893: user.username));
0894: } else {
0895: log(sm.getString("jndiRealm.authenticateFailure",
0896: user.username));
0897: }
0898: }
0899: return (validated);
0900: }
0901:
0902: /**
0903: * Check whether the credentials presented by the user match those
0904: * retrieved from the directory.
0905: *
0906: * @param context The directory context
0907: * @param user The User to be authenticated
0908: * @param credentials Authentication credentials
0909: *
0910: * @exception NamingException if a directory server error occurs
0911: */
0912: protected boolean compareCredentials(DirContext context, User info,
0913: String credentials) throws NamingException {
0914:
0915: if (info == null || credentials == null)
0916: return (false);
0917:
0918: String password = info.password;
0919: if (password == null)
0920: return (false);
0921:
0922: // Validate the credentials specified by the user
0923: if (debug >= 3)
0924: log(" validating credentials");
0925:
0926: boolean validated = false;
0927: if (hasMessageDigest()) {
0928: // Hex hashes should be compared case-insensitive
0929: validated = (digest(credentials).equalsIgnoreCase(password));
0930: } else
0931: validated = (digest(credentials).equals(password));
0932: return (validated);
0933:
0934: }
0935:
0936: /**
0937: * Check credentials by binding to the directory as the user
0938: *
0939: * @param context The directory context
0940: * @param user The User to be authenticated
0941: * @param credentials Authentication credentials
0942: *
0943: * @exception NamingException if a directory server error occurs
0944: */
0945: protected boolean bindAsUser(DirContext context, User user,
0946: String credentials) throws NamingException {
0947: Attributes attr;
0948:
0949: if (credentials == null || user == null)
0950: return (false);
0951:
0952: String dn = user.dn;
0953: if (dn == null)
0954: return (false);
0955:
0956: // Validate the credentials specified by the user
0957: if (debug >= 3) {
0958: log(" validating credentials by binding as the user");
0959: }
0960:
0961: // Set up security environment to bind as the user
0962: context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
0963: context.addToEnvironment(Context.SECURITY_CREDENTIALS,
0964: credentials);
0965:
0966: // Elicit an LDAP bind operation
0967: boolean validated = false;
0968: try {
0969: if (debug > 2) {
0970: log(" binding as " + dn);
0971: }
0972: attr = context.getAttributes("", null);
0973: validated = true;
0974: } catch (AuthenticationException e) {
0975: if (debug > 2) {
0976: log(" bind attempt failed");
0977: }
0978: }
0979:
0980: // Restore the original security environment
0981: if (connectionName != null) {
0982: context.addToEnvironment(Context.SECURITY_PRINCIPAL,
0983: connectionName);
0984: } else {
0985: context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
0986: }
0987:
0988: if (connectionPassword != null) {
0989: context.addToEnvironment(Context.SECURITY_CREDENTIALS,
0990: connectionPassword);
0991: } else {
0992: context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
0993: }
0994:
0995: return (validated);
0996: }
0997:
0998: /**
0999: * Return a List of roles associated with the given User. Any
1000: * roles present in the user's directory entry are supplemented by
1001: * a directory search. If no roles are associated with this user,
1002: * a zero-length List is returned.
1003: *
1004: * @param context The directory context we are searching
1005: * @param user The User to be checked
1006: *
1007: * @exception NamingException if a directory server error occurs
1008: */
1009: protected List getRoles(DirContext context, User user)
1010: throws NamingException {
1011:
1012: if (user == null)
1013: return (null);
1014:
1015: String dn = user.dn;
1016: String username = user.username;
1017:
1018: if (dn == null || username == null)
1019: return (null);
1020:
1021: if (debug >= 2)
1022: log(" getRoles(" + dn + ")");
1023:
1024: // Start with roles retrieved from the user entry
1025: ArrayList list = user.roles;
1026: if (list == null) {
1027: list = new ArrayList();
1028: }
1029:
1030: // Are we configured to do role searches?
1031: if ((roleFormat == null) || (roleName == null))
1032: return (list);
1033:
1034: // Set up parameters for an appropriate search
1035: String filter = roleFormat
1036: .format(new String[] { dn, username });
1037: SearchControls controls = new SearchControls();
1038: if (roleSubtree)
1039: controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
1040: else
1041: controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
1042: controls.setReturningAttributes(new String[] { roleName });
1043:
1044: // Perform the configured search and process the results
1045: if (debug >= 3) {
1046: log(" Searching role base '" + roleBase
1047: + "' for attribute '" + roleName + "'");
1048: log(" With filter expression '" + filter + "'");
1049: }
1050: NamingEnumeration results = context.search(roleBase, filter,
1051: controls);
1052: if (results == null)
1053: return (list); // Should never happen, but just in case ...
1054: while (results.hasMore()) {
1055: SearchResult result = (SearchResult) results.next();
1056: Attributes attrs = result.getAttributes();
1057: if (attrs == null)
1058: continue;
1059: list = addAttributeValues(roleName, attrs, list);
1060: }
1061:
1062: // Return the augmented list of roles
1063: if (debug >= 2) {
1064: log(" Returning " + list.size() + " roles");
1065: for (int i = 0; i < list.size(); i++)
1066: log(" Found role " + list.get(i));
1067: }
1068:
1069: return (list);
1070: }
1071:
1072: /**
1073: * Return a String representing the value of the specified attribute.
1074: *
1075: * @param attrId Attribute name
1076: * @param attrs Attributes containing the required value
1077: *
1078: * @exception NamingException if a directory server error occurs
1079: */
1080: private String getAttributeValue(String attrId, Attributes attrs)
1081: throws NamingException {
1082:
1083: if (debug >= 3)
1084: log(" retrieving attribute " + attrId);
1085:
1086: if (attrId == null || attrs == null)
1087: return null;
1088:
1089: Attribute attr = attrs.get(attrId);
1090: if (attr == null)
1091: return (null);
1092: Object value = attr.get();
1093: if (value == null)
1094: return (null);
1095: String valueString = null;
1096: if (value instanceof byte[])
1097: valueString = new String((byte[]) value);
1098: else
1099: valueString = value.toString();
1100:
1101: return valueString;
1102: }
1103:
1104: /**
1105: * Add values of a specified attribute to a list
1106: *
1107: * @param attrId Attribute name
1108: * @param attrs Attributes containing the new values
1109: * @param values ArrayList containing values found so far
1110: *
1111: * @exception NamingException if a directory server error occurs
1112: */
1113: private ArrayList addAttributeValues(String attrId,
1114: Attributes attrs, ArrayList values) throws NamingException {
1115:
1116: if (debug >= 3)
1117: log(" retrieving values for attribute " + attrId);
1118: if (attrId == null || attrs == null)
1119: return null;
1120: if (values == null)
1121: values = new ArrayList();
1122: Attribute attr = attrs.get(attrId);
1123: if (attr == null)
1124: return (null);
1125: NamingEnumeration e = attr.getAll();
1126: while (e.hasMore()) {
1127: String value = (String) e.next();
1128: values.add(value);
1129: }
1130: return values;
1131: }
1132:
1133: /**
1134: * Close any open connection to the directory server for this Realm.
1135: *
1136: * @param context The directory context to be closed
1137: */
1138: protected void close(DirContext context) {
1139:
1140: // Do nothing if there is no opened connection
1141: if (context == null)
1142: return;
1143:
1144: // Close our opened connection
1145: try {
1146: if (debug >= 1)
1147: log("Closing directory context");
1148: context.close();
1149: } catch (NamingException e) {
1150: log(sm.getString("jndiRealm.close"), e);
1151: }
1152: this .context = null;
1153:
1154: }
1155:
1156: /**
1157: * Return a short name for this Realm implementation.
1158: */
1159: protected String getName() {
1160:
1161: return (this .name);
1162:
1163: }
1164:
1165: /**
1166: * Return the password associated with the given principal's user name.
1167: */
1168: protected String getPassword(String username) {
1169:
1170: return (null);
1171:
1172: }
1173:
1174: /**
1175: * Return the Principal associated with the given user name.
1176: */
1177: protected Principal getPrincipal(String username) {
1178:
1179: return (null);
1180:
1181: }
1182:
1183: /**
1184: * Open (if necessary) and return a connection to the configured
1185: * directory server for this Realm.
1186: *
1187: * @exception NamingException if a directory server error occurs
1188: */
1189: protected DirContext open() throws NamingException {
1190:
1191: // Do nothing if there is a directory server connection already open
1192: if (context != null)
1193: return (context);
1194:
1195: // Establish a connection and retrieve the initial context
1196: if (debug >= 1)
1197: log("Connecting to URL " + connectionURL);
1198: Hashtable env = new Hashtable();
1199: env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
1200: if (connectionName != null)
1201: env.put(Context.SECURITY_PRINCIPAL, connectionName);
1202: if (connectionPassword != null)
1203: env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
1204: if (connectionURL != null)
1205: env.put(Context.PROVIDER_URL, connectionURL);
1206: context = new InitialDirContext(env);
1207: return (context);
1208:
1209: }
1210:
1211: /**
1212: * Release our use of this connection so that it can be recycled.
1213: *
1214: * @param context The directory context to release
1215: */
1216: protected void release(DirContext context) {
1217:
1218: ; // NO-OP since we are not pooling anything
1219:
1220: }
1221:
1222: // ------------------------------------------------------ Lifecycle Methods
1223:
1224: /**
1225: * Prepare for active use of the public methods of this Component.
1226: *
1227: * @exception LifecycleException if this component detects a fatal error
1228: * that prevents it from being started
1229: */
1230: public void start() throws LifecycleException {
1231:
1232: // Validate that we can open our connection
1233: try {
1234: open();
1235: } catch (NamingException e) {
1236: throw new LifecycleException(
1237: sm.getString("jndiRealm.open"), e);
1238: }
1239:
1240: // Perform normal superclass initialization
1241: super .start();
1242:
1243: }
1244:
1245: /**
1246: * Gracefully shut down active use of the public methods of this Component.
1247: *
1248: * @exception LifecycleException if this component detects a fatal error
1249: * that needs to be reported
1250: */
1251: public void stop() throws LifecycleException {
1252:
1253: // Perform normal superclass finalization
1254: super .stop();
1255:
1256: // Close any open directory server connection
1257: close(this .context);
1258:
1259: }
1260:
1261: }
1262:
1263: // ------------------------------------------------------ Private Classes
1264:
1265: /**
1266: * A private class representing a User
1267: */
1268: class User {
1269: String username = null;
1270: String dn = null;
1271: String password = null;
1272: ArrayList roles = null;
1273:
1274: User(String username, String dn, String password, ArrayList roles) {
1275: this.username = username;
1276: this.dn = dn;
1277: this.password = password;
1278: this.roles = roles;
1279: }
1280:
1281: }
|