0001: /*
0002: * Copyright 1999-2002,2004 The Apache Software Foundation.
0003: *
0004: * Licensed under the Apache License, Version 2.0 (the "License");
0005: * you may not use this file except in compliance with the License.
0006: * You may obtain a copy of the License at
0007: *
0008: * http://www.apache.org/licenses/LICENSE-2.0
0009: *
0010: * Unless required by applicable law or agreed to in writing, software
0011: * distributed under the License is distributed on an "AS IS" BASIS,
0012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0013: * See the License for the specific language governing permissions and
0014: * limitations under the License.
0015: */
0016:
0017: package org.apache.catalina.realm;
0018:
0019: import java.security.Principal;
0020: import java.text.MessageFormat;
0021: import java.util.ArrayList;
0022: import java.util.Hashtable;
0023: import java.util.List;
0024:
0025: import javax.naming.Context;
0026: import javax.naming.CommunicationException;
0027: import javax.naming.InvalidNameException;
0028: import javax.naming.NameNotFoundException;
0029: import javax.naming.NamingEnumeration;
0030: import javax.naming.NamingException;
0031: import javax.naming.NameParser;
0032: import javax.naming.Name;
0033: import javax.naming.AuthenticationException;
0034: import javax.naming.directory.Attribute;
0035: import javax.naming.directory.Attributes;
0036: import javax.naming.directory.DirContext;
0037: import javax.naming.directory.InitialDirContext;
0038: import javax.naming.directory.SearchControls;
0039: import javax.naming.directory.SearchResult;
0040: import org.apache.catalina.LifecycleException;
0041: import org.apache.catalina.util.Base64;
0042:
0043: /**
0044: * <p>Implementation of <strong>Realm</strong> that works with a directory
0045: * server accessed via the Java Naming and Directory Interface (JNDI) APIs.
0046: * The following constraints are imposed on the data structure in the
0047: * underlying directory server:</p>
0048: * <ul>
0049: *
0050: * <li>Each user that can be authenticated is represented by an individual
0051: * element in the top level <code>DirContext</code> that is accessed
0052: * via the <code>connectionURL</code> property.</li>
0053: *
0054: * <li>If a socket connection can not be made to the <code>connectURL</code>
0055: * an attempt will be made to use the <code>alternateURL</code> if it
0056: * exists.</li>
0057: *
0058: * <li>Each user element has a distinguished name that can be formed by
0059: * substituting the presented username into a pattern configured by the
0060: * <code>userPattern</code> property.</li>
0061: *
0062: * <li>Alternatively, if the <code>userPattern</code> property is not
0063: * specified, a unique element can be located by searching the directory
0064: * context. In this case:
0065: * <ul>
0066: * <li>The <code>userSearch</code> pattern specifies the search filter
0067: * after substitution of the username.</li>
0068: * <li>The <code>userBase</code> property can be set to the element that
0069: * is the base of the subtree containing users. If not specified,
0070: * the search base is the top-level context.</li>
0071: * <li>The <code>userSubtree</code> property can be set to
0072: * <code>true</code> if you wish to search the entire subtree of the
0073: * directory context. The default value of <code>false</code>
0074: * requests a search of only the current level.</li>
0075: * </ul>
0076: * </li>
0077: *
0078: * <li>The user may be authenticated by binding to the directory with the
0079: * username and password presented. This method is used when the
0080: * <code>userPassword</code> property is not specified.</li>
0081: *
0082: * <li>The user may be authenticated by retrieving the value of an attribute
0083: * from the directory and comparing it explicitly with the value presented
0084: * by the user. This method is used when the <code>userPassword</code>
0085: * property is specified, in which case:
0086: * <ul>
0087: * <li>The element for this user must contain an attribute named by the
0088: * <code>userPassword</code> property.
0089: * <li>The value of the user password attribute is either a cleartext
0090: * String, or the result of passing a cleartext String through the
0091: * <code>RealmBase.digest()</code> method (using the standard digest
0092: * support included in <code>RealmBase</code>).
0093: * <li>The user is considered to be authenticated if the presented
0094: * credentials (after being passed through
0095: * <code>RealmBase.digest()</code>) are equal to the retrieved value
0096: * for the user password attribute.</li>
0097: * </ul></li>
0098: *
0099: * <li>Each group of users that has been assigned a particular role may be
0100: * represented by an individual element in the top level
0101: * <code>DirContext</code> that is accessed via the
0102: * <code>connectionURL</code> property. This element has the following
0103: * characteristics:
0104: * <ul>
0105: * <li>The set of all possible groups of interest can be selected by a
0106: * search pattern configured by the <code>roleSearch</code>
0107: * property.</li>
0108: * <li>The <code>roleSearch</code> pattern optionally includes pattern
0109: * replacements "{0}" for the distinguished name, and/or "{1}" for
0110: * the username, of the authenticated user for which roles will be
0111: * retrieved.</li>
0112: * <li>The <code>roleBase</code> property can be set to the element that
0113: * is the base of the search for matching roles. If not specified,
0114: * the entire context will be searched.</li>
0115: * <li>The <code>roleSubtree</code> property can be set to
0116: * <code>true</code> if you wish to search the entire subtree of the
0117: * directory context. The default value of <code>false</code>
0118: * requests a search of only the current level.</li>
0119: * <li>The element includes an attribute (whose name is configured by
0120: * the <code>roleName</code> property) containing the name of the
0121: * role represented by this element.</li>
0122: * </ul></li>
0123: *
0124: * <li>In addition, roles may be represented by the values of an attribute
0125: * in the user's element whose name is configured by the
0126: * <code>userRoleName</code> property.</li>
0127: *
0128: * <li>Note that the standard <code><security-role-ref></code> element in
0129: * the web application deployment descriptor allows applications to refer
0130: * to roles programmatically by names other than those used in the
0131: * directory server itself.</li>
0132: * </ul>
0133: *
0134: * <p><strong>TODO</strong> - Support connection pooling (including message
0135: * format objects) so that <code>authenticate()</code> does not have to be
0136: * synchronized.</p>
0137: *
0138: * <p><strong>WARNING</strong> - There is a reported bug against the Netscape
0139: * provider code (com.netscape.jndi.ldap.LdapContextFactory) with respect to
0140: * successfully authenticated a non-existing user. The
0141: * report is here: http://nagoya.apache.org/bugzilla/show_bug.cgi?id=11210 .
0142: * With luck, Netscape has updated their provider code and this is not an
0143: * issue. </p>
0144: *
0145: * @author John Holman
0146: * @author Craig R. McClanahan
0147: * @version $Revision: 1.12 $ $Date: 2004/05/26 15:51:14 $
0148: */
0149:
0150: public class JNDIRealm extends RealmBase {
0151:
0152: // ----------------------------------------------------- Instance Variables
0153:
0154: /**
0155: * The type of authentication to use
0156: */
0157: protected String authentication = null;
0158:
0159: /**
0160: * The connection username for the server we will contact.
0161: */
0162: protected String connectionName = null;
0163:
0164: /**
0165: * The connection password for the server we will contact.
0166: */
0167: protected String connectionPassword = null;
0168:
0169: /**
0170: * The connection URL for the server we will contact.
0171: */
0172: protected String connectionURL = null;
0173:
0174: /**
0175: * The directory context linking us to our directory server.
0176: */
0177: protected DirContext context = null;
0178:
0179: /**
0180: * The JNDI context factory used to acquire our InitialContext. By
0181: * default, assumes use of an LDAP server using the standard JNDI LDAP
0182: * provider.
0183: */
0184: protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
0185:
0186: /**
0187: * Descriptive information about this Realm implementation.
0188: */
0189: protected static final String info = "org.apache.catalina.realm.JNDIRealm/1.0";
0190:
0191: /**
0192: * Descriptive information about this Realm implementation.
0193: */
0194: protected static final String name = "JNDIRealm";
0195:
0196: /**
0197: * The protocol that will be used in the communication with the
0198: * directory server.
0199: */
0200: protected String protocol = null;
0201:
0202: /**
0203: * How should we handle referrals? Microsoft Active Directory can't handle
0204: * the default case, so an application authenticating against AD must
0205: * set referrals to "follow".
0206: */
0207: protected String referrals = null;
0208:
0209: /**
0210: * The base element for user searches.
0211: */
0212: protected String userBase = "";
0213:
0214: /**
0215: * The message format used to search for a user, with "{0}" marking
0216: * the spot where the username goes.
0217: */
0218: protected String userSearch = null;
0219:
0220: /**
0221: * The MessageFormat object associated with the current
0222: * <code>userSearch</code>.
0223: */
0224: protected MessageFormat userSearchFormat = null;
0225:
0226: /**
0227: * Should we search the entire subtree for matching users?
0228: */
0229: protected boolean userSubtree = false;
0230:
0231: /**
0232: * The attribute name used to retrieve the user password.
0233: */
0234: protected String userPassword = null;
0235:
0236: /**
0237: * A string of LDAP user patterns or paths, ":"-separated
0238: * These will be used to form the distinguished name of a
0239: * user, with "{0}" marking the spot where the specified username
0240: * goes.
0241: * This is similar to userPattern, but allows for multiple searches
0242: * for a user.
0243: */
0244: protected String[] userPatternArray = null;
0245:
0246: /**
0247: * The message format used to form the distinguished name of a
0248: * user, with "{0}" marking the spot where the specified username
0249: * goes.
0250: */
0251: protected String userPattern = null;
0252:
0253: /**
0254: * An array of MessageFormat objects associated with the current
0255: * <code>userPatternArray</code>.
0256: */
0257: protected MessageFormat[] userPatternFormatArray = null;
0258:
0259: /**
0260: * The base element for role searches.
0261: */
0262: protected String roleBase = "";
0263:
0264: /**
0265: * The MessageFormat object associated with the current
0266: * <code>roleSearch</code>.
0267: */
0268: protected MessageFormat roleFormat = null;
0269:
0270: /**
0271: * The name of an attribute in the user's entry containing
0272: * roles for that user
0273: */
0274: protected String userRoleName = null;
0275:
0276: /**
0277: * The name of the attribute containing roles held elsewhere
0278: */
0279: protected String roleName = null;
0280:
0281: /**
0282: * The message format used to select roles for a user, with "{0}" marking
0283: * the spot where the distinguished name of the user goes.
0284: */
0285: protected String roleSearch = null;
0286:
0287: /**
0288: * Should we search the entire subtree for matching memberships?
0289: */
0290: protected boolean roleSubtree = false;
0291:
0292: /**
0293: * An alternate URL, to which, we should connect if connectionURL fails.
0294: */
0295: protected String alternateURL;
0296:
0297: /**
0298: * The number of connection attempts. If greater than zero we use the
0299: * alternate url.
0300: */
0301: protected int connectionAttempt = 0;
0302:
0303: /**
0304: * The current user pattern to be used for lookup and binding of a user.
0305: */
0306: protected int curUserPattern = 0;
0307:
0308: // ------------------------------------------------------------- Properties
0309:
0310: /**
0311: * Return the type of authentication to use.
0312: */
0313: public String getAuthentication() {
0314:
0315: return authentication;
0316:
0317: }
0318:
0319: /**
0320: * Set the type of authentication to use.
0321: *
0322: * @param authentication The authentication
0323: */
0324: public void setAuthentication(String authentication) {
0325:
0326: this .authentication = authentication;
0327:
0328: }
0329:
0330: /**
0331: * Return the connection username for this Realm.
0332: */
0333: public String getConnectionName() {
0334:
0335: return (this .connectionName);
0336:
0337: }
0338:
0339: /**
0340: * Set the connection username for this Realm.
0341: *
0342: * @param connectionName The new connection username
0343: */
0344: public void setConnectionName(String connectionName) {
0345:
0346: this .connectionName = connectionName;
0347:
0348: }
0349:
0350: /**
0351: * Return the connection password for this Realm.
0352: */
0353: public String getConnectionPassword() {
0354:
0355: return (this .connectionPassword);
0356:
0357: }
0358:
0359: /**
0360: * Set the connection password for this Realm.
0361: *
0362: * @param connectionPassword The new connection password
0363: */
0364: public void setConnectionPassword(String connectionPassword) {
0365:
0366: this .connectionPassword = connectionPassword;
0367:
0368: }
0369:
0370: /**
0371: * Return the connection URL for this Realm.
0372: */
0373: public String getConnectionURL() {
0374:
0375: return (this .connectionURL);
0376:
0377: }
0378:
0379: /**
0380: * Set the connection URL for this Realm.
0381: *
0382: * @param connectionURL The new connection URL
0383: */
0384: public void setConnectionURL(String connectionURL) {
0385:
0386: this .connectionURL = connectionURL;
0387:
0388: }
0389:
0390: /**
0391: * Return the JNDI context factory for this Realm.
0392: */
0393: public String getContextFactory() {
0394:
0395: return (this .contextFactory);
0396:
0397: }
0398:
0399: /**
0400: * Set the JNDI context factory for this Realm.
0401: *
0402: * @param contextFactory The new context factory
0403: */
0404: public void setContextFactory(String contextFactory) {
0405:
0406: this .contextFactory = contextFactory;
0407:
0408: }
0409:
0410: /**
0411: * Return the protocol to be used.
0412: */
0413: public String getProtocol() {
0414:
0415: return protocol;
0416:
0417: }
0418:
0419: /**
0420: * Set the protocol for this Realm.
0421: *
0422: * @param protocol The new protocol.
0423: */
0424: public void setProtocol(String protocol) {
0425:
0426: this .protocol = protocol;
0427:
0428: }
0429:
0430: /**
0431: * Returns the current settings for handling JNDI referrals.
0432: */
0433: public String getReferrals() {
0434: return referrals;
0435: }
0436:
0437: /**
0438: * How do we handle JNDI referrals? ignore, follow, or throw
0439: * (see javax.naming.Context.REFERRAL for more information).
0440: */
0441: public void setReferrals(String referrals) {
0442: this .referrals = referrals;
0443: }
0444:
0445: /**
0446: * Return the base element for user searches.
0447: */
0448: public String getUserBase() {
0449:
0450: return (this .userBase);
0451:
0452: }
0453:
0454: /**
0455: * Set the base element for user searches.
0456: *
0457: * @param userBase The new base element
0458: */
0459: public void setUserBase(String userBase) {
0460:
0461: this .userBase = userBase;
0462:
0463: }
0464:
0465: /**
0466: * Return the message format pattern for selecting users in this Realm.
0467: */
0468: public String getUserSearch() {
0469:
0470: return (this .userSearch);
0471:
0472: }
0473:
0474: /**
0475: * Set the message format pattern for selecting users in this Realm.
0476: *
0477: * @param userSearch The new user search pattern
0478: */
0479: public void setUserSearch(String userSearch) {
0480:
0481: this .userSearch = userSearch;
0482: if (userSearch == null)
0483: userSearchFormat = null;
0484: else
0485: userSearchFormat = new MessageFormat(userSearch);
0486:
0487: }
0488:
0489: /**
0490: * Return the "search subtree for users" flag.
0491: */
0492: public boolean getUserSubtree() {
0493:
0494: return (this .userSubtree);
0495:
0496: }
0497:
0498: /**
0499: * Set the "search subtree for users" flag.
0500: *
0501: * @param userSubtree The new search flag
0502: */
0503: public void setUserSubtree(boolean userSubtree) {
0504:
0505: this .userSubtree = userSubtree;
0506:
0507: }
0508:
0509: /**
0510: * Return the user role name attribute name for this Realm.
0511: */
0512: public String getUserRoleName() {
0513:
0514: return userRoleName;
0515: }
0516:
0517: /**
0518: * Set the user role name attribute name for this Realm.
0519: *
0520: * @param userRoleName The new userRole name attribute name
0521: */
0522: public void setUserRoleName(String userRoleName) {
0523:
0524: this .userRoleName = userRoleName;
0525:
0526: }
0527:
0528: /**
0529: * Return the base element for role searches.
0530: */
0531: public String getRoleBase() {
0532:
0533: return (this .roleBase);
0534:
0535: }
0536:
0537: /**
0538: * Set the base element for role searches.
0539: *
0540: * @param roleBase The new base element
0541: */
0542: public void setRoleBase(String roleBase) {
0543:
0544: this .roleBase = roleBase;
0545:
0546: }
0547:
0548: /**
0549: * Return the role name attribute name for this Realm.
0550: */
0551: public String getRoleName() {
0552:
0553: return (this .roleName);
0554:
0555: }
0556:
0557: /**
0558: * Set the role name attribute name for this Realm.
0559: *
0560: * @param roleName The new role name attribute name
0561: */
0562: public void setRoleName(String roleName) {
0563:
0564: this .roleName = roleName;
0565:
0566: }
0567:
0568: /**
0569: * Return the message format pattern for selecting roles in this Realm.
0570: */
0571: public String getRoleSearch() {
0572:
0573: return (this .roleSearch);
0574:
0575: }
0576:
0577: /**
0578: * Set the message format pattern for selecting roles in this Realm.
0579: *
0580: * @param roleSearch The new role search pattern
0581: */
0582: public void setRoleSearch(String roleSearch) {
0583:
0584: this .roleSearch = roleSearch;
0585: if (roleSearch == null)
0586: roleFormat = null;
0587: else
0588: roleFormat = new MessageFormat(roleSearch);
0589:
0590: }
0591:
0592: /**
0593: * Return the "search subtree for roles" flag.
0594: */
0595: public boolean getRoleSubtree() {
0596:
0597: return (this .roleSubtree);
0598:
0599: }
0600:
0601: /**
0602: * Set the "search subtree for roles" flag.
0603: *
0604: * @param roleSubtree The new search flag
0605: */
0606: public void setRoleSubtree(boolean roleSubtree) {
0607:
0608: this .roleSubtree = roleSubtree;
0609:
0610: }
0611:
0612: /**
0613: * Return the password attribute used to retrieve the user password.
0614: */
0615: public String getUserPassword() {
0616:
0617: return (this .userPassword);
0618:
0619: }
0620:
0621: /**
0622: * Set the password attribute used to retrieve the user password.
0623: *
0624: * @param userPassword The new password attribute
0625: */
0626: public void setUserPassword(String userPassword) {
0627:
0628: this .userPassword = userPassword;
0629:
0630: }
0631:
0632: /**
0633: * Return the message format pattern for selecting users in this Realm.
0634: */
0635: public String getUserPattern() {
0636:
0637: return (this .userPattern);
0638:
0639: }
0640:
0641: /**
0642: * Set the message format pattern for selecting users in this Realm.
0643: * This may be one simple pattern, or multiple patterns to be tried,
0644: * separated by parentheses. (for example, either "cn={0}", or
0645: * "(cn={0})(cn={0},o=myorg)" Full LDAP search strings are also supported,
0646: * but only the "OR", "|" syntax, so "(|(cn={0})(cn={0},o=myorg))" is
0647: * also valid. Complex search strings with &, etc are NOT supported.
0648: *
0649: * @param userPattern The new user pattern
0650: */
0651: public void setUserPattern(String userPattern) {
0652:
0653: this .userPattern = userPattern;
0654: if (userPattern == null)
0655: userPatternArray = null;
0656: else {
0657: userPatternArray = parseUserPatternString(userPattern);
0658: int len = this .userPatternArray.length;
0659: userPatternFormatArray = new MessageFormat[len];
0660: for (int i = 0; i < len; i++) {
0661: userPatternFormatArray[i] = new MessageFormat(
0662: userPatternArray[i]);
0663: }
0664: }
0665: }
0666:
0667: /**
0668: * Getter for property alternateURL.
0669: *
0670: * @return Value of property alternateURL.
0671: */
0672: public String getAlternateURL() {
0673:
0674: return this .alternateURL;
0675:
0676: }
0677:
0678: /**
0679: * Setter for property alternateURL.
0680: *
0681: * @param alternateURL New value of property alternateURL.
0682: */
0683: public void setAlternateURL(String alternateURL) {
0684:
0685: this .alternateURL = alternateURL;
0686:
0687: }
0688:
0689: // ---------------------------------------------------------- Realm Methods
0690:
0691: /**
0692: * Return the Principal associated with the specified username and
0693: * credentials, if there is one; otherwise return <code>null</code>.
0694: *
0695: * If there are any errors with the JDBC connection, executing
0696: * the query or anything we return null (don't authenticate). This
0697: * event is also logged, and the connection will be closed so that
0698: * a subsequent request will automatically re-open it.
0699: *
0700: * @param username Username of the Principal to look up
0701: * @param credentials Password or other credentials to use in
0702: * authenticating this username
0703: */
0704: public Principal authenticate(String username, String credentials) {
0705:
0706: DirContext context = null;
0707: Principal principal = null;
0708:
0709: try {
0710:
0711: // Ensure that we have a directory context available
0712: context = open();
0713:
0714: // Occassionally the directory context will timeout. Try one more
0715: // time before giving up.
0716: try {
0717:
0718: // Authenticate the specified username if possible
0719: principal = authenticate(context, username, credentials);
0720:
0721: } catch (CommunicationException e) {
0722:
0723: // If contains the work closed. Then assume socket is closed.
0724: // If message is null, assume the worst and allow the
0725: // connection to be closed.
0726: if (e.getMessage() != null
0727: && e.getMessage().indexOf("closed") < 0)
0728: throw (e);
0729:
0730: // log the exception so we know it's there.
0731: log(sm.getString("jndiRealm.exception"), e);
0732:
0733: // close the connection so we know it will be reopened.
0734: if (context != null)
0735: close(context);
0736:
0737: // open a new directory context.
0738: context = open();
0739:
0740: // Try the authentication again.
0741: principal = authenticate(context, username, credentials);
0742:
0743: }
0744:
0745: // Release this context
0746: release(context);
0747:
0748: // Return the authenticated Principal (if any)
0749: return (principal);
0750:
0751: } catch (NamingException e) {
0752:
0753: // Log the problem for posterity
0754: log(sm.getString("jndiRealm.exception"), e);
0755:
0756: // Close the connection so that it gets reopened next time
0757: if (context != null)
0758: close(context);
0759:
0760: // Return "not authenticated" for this request
0761: return (null);
0762:
0763: }
0764:
0765: }
0766:
0767: // -------------------------------------------------------- Package Methods
0768:
0769: // ------------------------------------------------------ Protected Methods
0770:
0771: /**
0772: * Return the Principal associated with the specified username and
0773: * credentials, if there is one; otherwise return <code>null</code>.
0774: *
0775: * @param context The directory context
0776: * @param username Username of the Principal to look up
0777: * @param credentials Password or other credentials to use in
0778: * authenticating this username
0779: *
0780: * @exception NamingException if a directory server error occurs
0781: */
0782: public synchronized Principal authenticate(DirContext context,
0783: String username, String credentials) throws NamingException {
0784:
0785: if (username == null || username.equals("")
0786: || credentials == null || credentials.equals(""))
0787: return (null);
0788:
0789: if (userPatternArray != null) {
0790: for (curUserPattern = 0; curUserPattern < userPatternFormatArray.length; curUserPattern++) {
0791: // Retrieve user information
0792: User user = getUser(context, username);
0793: if (user != null) {
0794: try {
0795: // Check the user's credentials
0796: if (checkCredentials(context, user, credentials)) {
0797: // Search for additional roles
0798: List roles = getRoles(context, user);
0799: return (new GenericPrincipal(this ,
0800: username, credentials, roles));
0801: }
0802: } catch (InvalidNameException ine) {
0803: // Log the problem for posterity
0804: log(sm.getString("jndiRealm.exception"), ine);
0805: // ignore; this is probably due to a name not fitting
0806: // the search path format exactly, as in a fully-
0807: // qualified name being munged into a search path
0808: // that already contains cn= or vice-versa
0809: }
0810: }
0811: }
0812: return null;
0813: } else {
0814: // Retrieve user information
0815: User user = getUser(context, username);
0816: if (user == null)
0817: return (null);
0818:
0819: // Check the user's credentials
0820: if (!checkCredentials(context, user, credentials))
0821: return (null);
0822:
0823: // Search for additional roles
0824: List roles = getRoles(context, user);
0825:
0826: // Create and return a suitable Principal for this user
0827: return (new GenericPrincipal(this , username, credentials,
0828: roles));
0829: }
0830: }
0831:
0832: /**
0833: * Return a User object containing information about the user
0834: * with the specified username, if found in the directory;
0835: * otherwise return <code>null</code>.
0836: *
0837: * If the <code>userPassword</code> configuration attribute is
0838: * specified, the value of that attribute is retrieved from the
0839: * user's directory entry. If the <code>userRoleName</code>
0840: * configuration attribute is specified, all values of that
0841: * attribute are retrieved from the directory entry.
0842: *
0843: * @param context The directory context
0844: * @param username Username to be looked up
0845: *
0846: * @exception NamingException if a directory server error occurs
0847: */
0848: protected User getUser(DirContext context, String username)
0849: throws NamingException {
0850:
0851: User user = null;
0852:
0853: // Get attributes to retrieve from user entry
0854: ArrayList list = new ArrayList();
0855: if (userPassword != null)
0856: list.add(userPassword);
0857: if (userRoleName != null)
0858: list.add(userRoleName);
0859: String[] attrIds = new String[list.size()];
0860: list.toArray(attrIds);
0861:
0862: // Use pattern or search for user entry
0863: if (userPatternFormatArray != null) {
0864: user = getUserByPattern(context, username, attrIds);
0865: } else {
0866: user = getUserBySearch(context, username, attrIds);
0867: }
0868:
0869: return user;
0870: }
0871:
0872: /**
0873: * Use the <code>UserPattern</code> configuration attribute to
0874: * locate the directory entry for the user with the specified
0875: * username and return a User object; otherwise return
0876: * <code>null</code>.
0877: *
0878: * @param context The directory context
0879: * @param username The username
0880: * @param attrIds String[]containing names of attributes to
0881: * retrieve.
0882: *
0883: * @exception NamingException if a directory server error occurs
0884: */
0885: protected User getUserByPattern(DirContext context,
0886: String username, String[] attrIds) throws NamingException {
0887:
0888: if (debug >= 2)
0889: log("lookupUser(" + username + ")");
0890:
0891: if (username == null
0892: || userPatternFormatArray[curUserPattern] == null)
0893: return (null);
0894:
0895: // Form the dn from the user pattern
0896: String dn = userPatternFormatArray[curUserPattern]
0897: .format(new String[] { username });
0898: if (debug >= 3) {
0899: log(" dn=" + dn);
0900: }
0901:
0902: // Return if no attributes to retrieve
0903: if (attrIds == null || attrIds.length == 0)
0904: return new User(username, dn, null, null);
0905:
0906: // Get required attributes from user entry
0907: Attributes attrs = null;
0908: try {
0909: attrs = context.getAttributes(dn, attrIds);
0910: } catch (NameNotFoundException e) {
0911: return (null);
0912: }
0913: if (attrs == null)
0914: return (null);
0915:
0916: // Retrieve value of userPassword
0917: String password = null;
0918: if (userPassword != null)
0919: password = getAttributeValue(userPassword, attrs);
0920:
0921: // Retrieve values of userRoleName attribute
0922: ArrayList roles = null;
0923: if (userRoleName != null)
0924: roles = addAttributeValues(userRoleName, attrs, roles);
0925:
0926: return new User(username, dn, password, roles);
0927: }
0928:
0929: /**
0930: * Search the directory to return a User object containing
0931: * information about the user with the specified username, if
0932: * found in the directory; otherwise return <code>null</code>.
0933: *
0934: * @param context The directory context
0935: * @param username The username
0936: * @param attrIds String[]containing names of attributes to retrieve.
0937: *
0938: * @exception NamingException if a directory server error occurs
0939: */
0940: protected User getUserBySearch(DirContext context, String username,
0941: String[] attrIds) throws NamingException {
0942:
0943: if (username == null || userSearchFormat == null)
0944: return (null);
0945:
0946: // Form the search filter
0947: String filter = userSearchFormat
0948: .format(new String[] { username });
0949:
0950: // Set up the search controls
0951: SearchControls constraints = new SearchControls();
0952:
0953: if (userSubtree) {
0954: constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
0955: } else {
0956: constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
0957: }
0958:
0959: // Specify the attributes to be retrieved
0960: if (attrIds == null)
0961: attrIds = new String[0];
0962: constraints.setReturningAttributes(attrIds);
0963:
0964: if (debug > 3) {
0965: log(" Searching for " + username);
0966: log(" base: " + userBase + " filter: " + filter);
0967: }
0968:
0969: NamingEnumeration results = context.search(userBase, filter,
0970: constraints);
0971:
0972: // Fail if no entries found
0973: if (results == null || !results.hasMore()) {
0974: if (debug > 2) {
0975: log(" username not found");
0976: }
0977: return (null);
0978: }
0979:
0980: // Get result for the first entry found
0981: SearchResult result = (SearchResult) results.next();
0982:
0983: // Check no further entries were found
0984: if (results.hasMore()) {
0985: log("username " + username + " has multiple entries");
0986: return (null);
0987: }
0988:
0989: // Get the entry's distinguished name
0990: NameParser parser = context.getNameParser("");
0991: Name contextName = parser.parse(context.getNameInNamespace());
0992: Name baseName = parser.parse(userBase);
0993: Name entryName = parser.parse(result.getName());
0994: Name name = contextName.addAll(baseName);
0995: name = name.addAll(entryName);
0996: String dn = name.toString();
0997:
0998: if (debug > 2)
0999: log(" entry found for " + username + " with dn " + dn);
1000:
1001: // Get the entry's attributes
1002: Attributes attrs = result.getAttributes();
1003: if (attrs == null)
1004: return null;
1005:
1006: // Retrieve value of userPassword
1007: String password = null;
1008: if (userPassword != null)
1009: password = getAttributeValue(userPassword, attrs);
1010:
1011: // Retrieve values of userRoleName attribute
1012: ArrayList roles = null;
1013: if (userRoleName != null)
1014: roles = addAttributeValues(userRoleName, attrs, roles);
1015:
1016: return new User(username, dn, password, roles);
1017: }
1018:
1019: /**
1020: * Check whether the given User can be authenticated with the
1021: * given credentials. If the <code>userPassword</code>
1022: * configuration attribute is specified, the credentials
1023: * previously retrieved from the directory are compared explicitly
1024: * with those presented by the user. Otherwise the presented
1025: * credentials are checked by binding to the directory as the
1026: * user.
1027: *
1028: * @param context The directory context
1029: * @param user The User to be authenticated
1030: * @param credentials The credentials presented by the user
1031: *
1032: * @exception NamingException if a directory server error occurs
1033: */
1034: protected boolean checkCredentials(DirContext context, User user,
1035: String credentials) throws NamingException {
1036:
1037: boolean validated = false;
1038:
1039: if (userPassword == null) {
1040: validated = bindAsUser(context, user, credentials);
1041: } else {
1042: validated = compareCredentials(context, user, credentials);
1043: }
1044:
1045: if (debug >= 2) {
1046: if (validated) {
1047: log(sm.getString("jndiRealm.authenticateSuccess",
1048: user.username));
1049: } else {
1050: log(sm.getString("jndiRealm.authenticateFailure",
1051: user.username));
1052: }
1053: }
1054: return (validated);
1055: }
1056:
1057: /**
1058: * Check whether the credentials presented by the user match those
1059: * retrieved from the directory.
1060: *
1061: * @param context The directory context
1062: * @param info The User to be authenticated
1063: * @param credentials Authentication credentials
1064: *
1065: * @exception NamingException if a directory server error occurs
1066: */
1067: protected boolean compareCredentials(DirContext context, User info,
1068: String credentials) throws NamingException {
1069:
1070: if (info == null || credentials == null)
1071: return (false);
1072:
1073: String password = info.password;
1074: if (password == null)
1075: return (false);
1076:
1077: // Validate the credentials specified by the user
1078: if (debug >= 3)
1079: log(" validating credentials");
1080:
1081: boolean validated = false;
1082: if (hasMessageDigest()) {
1083: // iPlanet support if the values starts with {SHA1}
1084: // The string is in a format compatible with Base64.encode not
1085: // the Hex encoding of the parent class.
1086: if (password.startsWith("{SHA}")) {
1087: /* sync since super.digest() does this same thing */
1088: synchronized (this ) {
1089: password = password.substring(5);
1090: md.reset();
1091: md.update(credentials.getBytes());
1092: String digestedPassword = new String(Base64
1093: .encode(md.digest()));
1094: validated = password.equals(digestedPassword);
1095: }
1096: } else {
1097: // Hex hashes should be compared case-insensitive
1098: validated = (digest(credentials)
1099: .equalsIgnoreCase(password));
1100: }
1101: } else
1102: validated = (digest(credentials).equals(password));
1103: return (validated);
1104:
1105: }
1106:
1107: /**
1108: * Check credentials by binding to the directory as the user
1109: *
1110: * @param context The directory context
1111: * @param user The User to be authenticated
1112: * @param credentials Authentication credentials
1113: *
1114: * @exception NamingException if a directory server error occurs
1115: */
1116: protected boolean bindAsUser(DirContext context, User user,
1117: String credentials) throws NamingException {
1118: Attributes attr;
1119:
1120: if (credentials == null || user == null)
1121: return (false);
1122:
1123: String dn = user.dn;
1124: if (dn == null)
1125: return (false);
1126:
1127: // Validate the credentials specified by the user
1128: if (debug >= 3) {
1129: log(" validating credentials by binding as the user");
1130: }
1131:
1132: // Set up security environment to bind as the user
1133: context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
1134: context.addToEnvironment(Context.SECURITY_CREDENTIALS,
1135: credentials);
1136:
1137: // Elicit an LDAP bind operation
1138: boolean validated = false;
1139: try {
1140: if (debug > 2) {
1141: log(" binding as " + dn);
1142: }
1143: attr = context.getAttributes("", null);
1144: validated = true;
1145: } catch (AuthenticationException e) {
1146: if (debug > 2) {
1147: log(" bind attempt failed");
1148: }
1149: }
1150:
1151: // Restore the original security environment
1152: if (connectionName != null) {
1153: context.addToEnvironment(Context.SECURITY_PRINCIPAL,
1154: connectionName);
1155: } else {
1156: context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
1157: }
1158:
1159: if (connectionPassword != null) {
1160: context.addToEnvironment(Context.SECURITY_CREDENTIALS,
1161: connectionPassword);
1162: } else {
1163: context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
1164: }
1165:
1166: return (validated);
1167: }
1168:
1169: /**
1170: * Return a List of roles associated with the given User. Any
1171: * roles present in the user's directory entry are supplemented by
1172: * a directory search. If no roles are associated with this user,
1173: * a zero-length List is returned.
1174: *
1175: * @param context The directory context we are searching
1176: * @param user The User to be checked
1177: *
1178: * @exception NamingException if a directory server error occurs
1179: */
1180: protected List getRoles(DirContext context, User user)
1181: throws NamingException {
1182:
1183: if (user == null)
1184: return (null);
1185:
1186: String dn = user.dn;
1187: String username = user.username;
1188:
1189: if (dn == null || username == null)
1190: return (null);
1191:
1192: if (debug >= 2)
1193: log(" getRoles(" + dn + ")");
1194:
1195: // Start with roles retrieved from the user entry
1196: ArrayList list = user.roles;
1197: if (list == null) {
1198: list = new ArrayList();
1199: }
1200:
1201: // Are we configured to do role searches?
1202: if ((roleFormat == null) || (roleName == null))
1203: return (list);
1204:
1205: // Set up parameters for an appropriate search
1206: String filter = roleFormat.format(new String[] {
1207: doRFC2254Encoding(dn), username });
1208: SearchControls controls = new SearchControls();
1209: if (roleSubtree)
1210: controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
1211: else
1212: controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
1213: controls.setReturningAttributes(new String[] { roleName });
1214:
1215: // Perform the configured search and process the results
1216: if (debug >= 3) {
1217: log(" Searching role base '" + roleBase
1218: + "' for attribute '" + roleName + "'");
1219: log(" With filter expression '" + filter + "'");
1220: }
1221: NamingEnumeration results = context.search(roleBase, filter,
1222: controls);
1223: if (results == null)
1224: return (list); // Should never happen, but just in case ...
1225: while (results.hasMore()) {
1226: SearchResult result = (SearchResult) results.next();
1227: Attributes attrs = result.getAttributes();
1228: if (attrs == null)
1229: continue;
1230: list = addAttributeValues(roleName, attrs, list);
1231: }
1232:
1233: if (debug >= 2) {
1234: if (list != null) {
1235: log(" Returning " + list.size() + " roles");
1236: for (int i = 0; i < list.size(); i++)
1237: log(" Found role " + list.get(i));
1238: } else {
1239: log(" getRoles about to return null ");
1240: }
1241: }
1242:
1243: return (list);
1244: }
1245:
1246: /**
1247: * Return a String representing the value of the specified attribute.
1248: *
1249: * @param attrId Attribute name
1250: * @param attrs Attributes containing the required value
1251: *
1252: * @exception NamingException if a directory server error occurs
1253: */
1254: private String getAttributeValue(String attrId, Attributes attrs)
1255: throws NamingException {
1256:
1257: if (debug >= 3)
1258: log(" retrieving attribute " + attrId);
1259:
1260: if (attrId == null || attrs == null)
1261: return null;
1262:
1263: Attribute attr = attrs.get(attrId);
1264: if (attr == null)
1265: return (null);
1266: Object value = attr.get();
1267: if (value == null)
1268: return (null);
1269: String valueString = null;
1270: if (value instanceof byte[])
1271: valueString = new String((byte[]) value);
1272: else
1273: valueString = value.toString();
1274:
1275: return valueString;
1276: }
1277:
1278: /**
1279: * Add values of a specified attribute to a list
1280: *
1281: * @param attrId Attribute name
1282: * @param attrs Attributes containing the new values
1283: * @param values ArrayList containing values found so far
1284: *
1285: * @exception NamingException if a directory server error occurs
1286: */
1287: private ArrayList addAttributeValues(String attrId,
1288: Attributes attrs, ArrayList values) throws NamingException {
1289:
1290: if (debug >= 3)
1291: log(" retrieving values for attribute " + attrId);
1292: if (attrId == null || attrs == null)
1293: return values;
1294: if (values == null)
1295: values = new ArrayList();
1296: Attribute attr = attrs.get(attrId);
1297: if (attr == null)
1298: return (values);
1299: NamingEnumeration e = attr.getAll();
1300: while (e.hasMore()) {
1301: String value = (String) e.next();
1302: values.add(value);
1303: }
1304: return values;
1305: }
1306:
1307: /**
1308: * Close any open connection to the directory server for this Realm.
1309: *
1310: * @param context The directory context to be closed
1311: */
1312: protected void close(DirContext context) {
1313:
1314: // Do nothing if there is no opened connection
1315: if (context == null)
1316: return;
1317:
1318: // Close our opened connection
1319: try {
1320: if (debug >= 1)
1321: log("Closing directory context");
1322: context.close();
1323: } catch (NamingException e) {
1324: log(sm.getString("jndiRealm.close"), e);
1325: }
1326: this .context = null;
1327:
1328: }
1329:
1330: /**
1331: * Return a short name for this Realm implementation.
1332: */
1333: protected String getName() {
1334:
1335: return (name);
1336:
1337: }
1338:
1339: /**
1340: * Return the password associated with the given principal's user name.
1341: */
1342: protected String getPassword(String username) {
1343:
1344: return (null);
1345:
1346: }
1347:
1348: /**
1349: * Return the Principal associated with the given user name.
1350: */
1351: protected Principal getPrincipal(String username) {
1352:
1353: return (null);
1354:
1355: }
1356:
1357: /**
1358: * Open (if necessary) and return a connection to the configured
1359: * directory server for this Realm.
1360: *
1361: * @exception NamingException if a directory server error occurs
1362: */
1363: protected DirContext open() throws NamingException {
1364:
1365: // Do nothing if there is a directory server connection already open
1366: if (context != null)
1367: return (context);
1368:
1369: try {
1370:
1371: // Ensure that we have a directory context available
1372: context = new InitialDirContext(
1373: getDirectoryContextEnvironment());
1374:
1375: } catch (NamingException e) {
1376:
1377: connectionAttempt = 1;
1378:
1379: // log the first exception.
1380: log(sm.getString("jndiRealm.exception"), e);
1381:
1382: // Try connecting to the alternate url.
1383: context = new InitialDirContext(
1384: getDirectoryContextEnvironment());
1385:
1386: } finally {
1387:
1388: // reset it in case the connection times out.
1389: // the primary may come back.
1390: connectionAttempt = 0;
1391:
1392: }
1393:
1394: return (context);
1395:
1396: }
1397:
1398: /**
1399: * Create our directory context configuration.
1400: *
1401: * @return java.util.Hashtable the configuration for the directory context.
1402: */
1403: protected Hashtable getDirectoryContextEnvironment() {
1404:
1405: Hashtable env = new Hashtable();
1406:
1407: // Configure our directory context environment.
1408: if (debug >= 1 && connectionAttempt == 0)
1409: log("Connecting to URL " + connectionURL);
1410: else if (debug >= 1 && connectionAttempt > 0)
1411: log("Connecting to URL " + alternateURL);
1412: env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
1413: if (connectionName != null)
1414: env.put(Context.SECURITY_PRINCIPAL, connectionName);
1415: if (connectionPassword != null)
1416: env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
1417: if (connectionURL != null && connectionAttempt == 0)
1418: env.put(Context.PROVIDER_URL, connectionURL);
1419: else if (alternateURL != null && connectionAttempt > 0)
1420: env.put(Context.PROVIDER_URL, alternateURL);
1421: if (authentication != null)
1422: env.put(Context.SECURITY_AUTHENTICATION, authentication);
1423: if (protocol != null)
1424: env.put(Context.SECURITY_PROTOCOL, protocol);
1425: if (referrals != null)
1426: env.put(Context.REFERRAL, referrals);
1427:
1428: return env;
1429:
1430: }
1431:
1432: /**
1433: * Release our use of this connection so that it can be recycled.
1434: *
1435: * @param context The directory context to release
1436: */
1437: protected void release(DirContext context) {
1438:
1439: ; // NO-OP since we are not pooling anything
1440:
1441: }
1442:
1443: // ------------------------------------------------------ Lifecycle Methods
1444:
1445: /**
1446: * Prepare for active use of the public methods of this Component.
1447: *
1448: * @exception LifecycleException if this component detects a fatal error
1449: * that prevents it from being started
1450: */
1451: public void start() throws LifecycleException {
1452:
1453: // Validate that we can open our connection
1454: try {
1455: open();
1456: } catch (NamingException e) {
1457: throw new LifecycleException(
1458: sm.getString("jndiRealm.open"), e);
1459: }
1460:
1461: // Perform normal superclass initialization
1462: super .start();
1463:
1464: }
1465:
1466: /**
1467: * Gracefully shut down active use of the public methods of this Component.
1468: *
1469: * @exception LifecycleException if this component detects a fatal error
1470: * that needs to be reported
1471: */
1472: public void stop() throws LifecycleException {
1473:
1474: // Perform normal superclass finalization
1475: super .stop();
1476:
1477: // Close any open directory server connection
1478: close(this .context);
1479:
1480: }
1481:
1482: /**
1483: * Given a string containing LDAP patterns for user locations (separated by
1484: * parentheses in a pseudo-LDAP search string format -
1485: * "(location1)(location2)", returns an array of those paths. Real LDAP
1486: * search strings are supported as well (though only the "|" "OR" type).
1487: *
1488: * @param userPatternString - a string LDAP search paths surrounded by
1489: * parentheses
1490: */
1491: protected String[] parseUserPatternString(String userPatternString) {
1492:
1493: if (userPatternString != null) {
1494: ArrayList pathList = new ArrayList();
1495: int startParenLoc = userPatternString.indexOf('(');
1496: if (startParenLoc == -1) {
1497: // no parens here; return whole thing
1498: return new String[] { userPatternString };
1499: }
1500: int startingPoint = 0;
1501: while (startParenLoc > -1) {
1502: int endParenLoc = 0;
1503: // weed out escaped open parens and parens enclosing the
1504: // whole statement (in the case of valid LDAP search
1505: // strings: (|(something)(somethingelse))
1506: while ((userPatternString.charAt(startParenLoc + 1) == '|')
1507: || (startParenLoc != 0 && userPatternString
1508: .charAt(startParenLoc - 1) == '\\')) {
1509: startParenLoc = userPatternString.indexOf("(",
1510: startParenLoc + 1);
1511: }
1512: endParenLoc = userPatternString.indexOf(")",
1513: startParenLoc + 1);
1514: // weed out escaped end-parens
1515: while (userPatternString.charAt(endParenLoc - 1) == '\\') {
1516: endParenLoc = userPatternString.indexOf(")",
1517: endParenLoc + 1);
1518: }
1519: String nextPathPart = userPatternString.substring(
1520: startParenLoc + 1, endParenLoc);
1521: pathList.add(nextPathPart);
1522: startingPoint = endParenLoc + 1;
1523: startParenLoc = userPatternString.indexOf('(',
1524: startingPoint);
1525: }
1526: return (String[]) pathList.toArray(new String[] {});
1527: }
1528: return null;
1529:
1530: }
1531:
1532: /**
1533: * Given an LDAP search string, returns the string with certain characters
1534: * escaped according to RFC 2254 guidelines.
1535: * The character mapping is as follows:
1536: * char -> Replacement
1537: * ---------------------------
1538: * * -> \2a
1539: * ( -> \28
1540: * ) -> \29
1541: * \ -> \5c
1542: * \0 -> \00
1543: * @param inString string to escape according to RFC 2254 guidelines
1544: * @return String the escaped/encoded result
1545: */
1546: protected String doRFC2254Encoding(String inString) {
1547: StringBuffer buf = new StringBuffer(inString.length());
1548: for (int i = 0; i < inString.length(); i++) {
1549: char c = inString.charAt(i);
1550: switch (c) {
1551: case '\\':
1552: buf.append("\\5c");
1553: break;
1554: case '*':
1555: buf.append("\\2a");
1556: break;
1557: case '(':
1558: buf.append("\\28");
1559: break;
1560: case ')':
1561: buf.append("\\29");
1562: break;
1563: case '\0':
1564: buf.append("\\00");
1565: break;
1566: default:
1567: buf.append(c);
1568: break;
1569: }
1570: }
1571: return buf.toString();
1572: }
1573:
1574: }
1575:
1576: // ------------------------------------------------------ Private Classes
1577:
1578: /**
1579: * A private class representing a User
1580: */
1581: class User {
1582: String username = null;
1583: String dn = null;
1584: String password = null;
1585: ArrayList roles = null;
1586:
1587: User(String username, String dn, String password, ArrayList roles) {
1588: this.username = username;
1589: this.dn = dn;
1590: this.password = password;
1591: this.roles = roles;
1592: }
1593:
1594: }
|