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