| /* |
| * Copyright 1999-2002,2004 The Apache Software Foundation. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| |
| |
| |
| package org.apache.catalina.realm; |
| |
| |
| import java.security.Principal; |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.Hashtable; |
| import java.util.List; |
| |
| import javax.naming.Context; |
| import javax.naming.CommunicationException; |
| import javax.naming.InvalidNameException; |
| import javax.naming.NameNotFoundException; |
| import javax.naming.NamingEnumeration; |
| import javax.naming.NamingException; |
| import javax.naming.NameParser; |
| import javax.naming.Name; |
| import javax.naming.AuthenticationException; |
| import javax.naming.directory.Attribute; |
| import javax.naming.directory.Attributes; |
| import javax.naming.directory.DirContext; |
| import javax.naming.directory.InitialDirContext; |
| import javax.naming.directory.SearchControls; |
| import javax.naming.directory.SearchResult; |
| import org.apache.catalina.LifecycleException; |
| import org.apache.catalina.util.Base64; |
| |
| |
| /** |
| * <p>Implementation of <strong>Realm</strong> that works with a directory |
| * server accessed via the Java Naming and Directory Interface (JNDI) APIs. |
| * The following constraints are imposed on the data structure in the |
| * underlying directory server:</p> |
| * <ul> |
| * |
| * <li>Each user that can be authenticated is represented by an individual |
| * element in the top level <code>DirContext</code> that is accessed |
| * via the <code>connectionURL</code> property.</li> |
| * |
| * <li>If a socket connection can not be made to the <code>connectURL</code> |
| * an attempt will be made to use the <code>alternateURL</code> if it |
| * exists.</li> |
| * |
| * <li>Each user element has a distinguished name that can be formed by |
| * substituting the presented username into a pattern configured by the |
| * <code>userPattern</code> property.</li> |
| * |
| * <li>Alternatively, if the <code>userPattern</code> property is not |
| * specified, a unique element can be located by searching the directory |
| * context. In this case: |
| * <ul> |
| * <li>The <code>userSearch</code> pattern specifies the search filter |
| * after substitution of the username.</li> |
| * <li>The <code>userBase</code> property can be set to the element that |
| * is the base of the subtree containing users. If not specified, |
| * the search base is the top-level context.</li> |
| * <li>The <code>userSubtree</code> property can be set to |
| * <code>true</code> if you wish to search the entire subtree of the |
| * directory context. The default value of <code>false</code> |
| * requests a search of only the current level.</li> |
| * </ul> |
| * </li> |
| * |
| * <li>The user may be authenticated by binding to the directory with the |
| * username and password presented. This method is used when the |
| * <code>userPassword</code> property is not specified.</li> |
| * |
| * <li>The user may be authenticated by retrieving the value of an attribute |
| * from the directory and comparing it explicitly with the value presented |
| * by the user. This method is used when the <code>userPassword</code> |
| * property is specified, in which case: |
| * <ul> |
| * <li>The element for this user must contain an attribute named by the |
| * <code>userPassword</code> property. |
| * <li>The value of the user password attribute is either a cleartext |
| * String, or the result of passing a cleartext String through the |
| * <code>RealmBase.digest()</code> method (using the standard digest |
| * support included in <code>RealmBase</code>). |
| * <li>The user is considered to be authenticated if the presented |
| * credentials (after being passed through |
| * <code>RealmBase.digest()</code>) are equal to the retrieved value |
| * for the user password attribute.</li> |
| * </ul></li> |
| * |
| * <li>Each group of users that has been assigned a particular role may be |
| * represented by an individual element in the top level |
| * <code>DirContext</code> that is accessed via the |
| * <code>connectionURL</code> property. This element has the following |
| * characteristics: |
| * <ul> |
| * <li>The set of all possible groups of interest can be selected by a |
| * search pattern configured by the <code>roleSearch</code> |
| * property.</li> |
| * <li>The <code>roleSearch</code> pattern optionally includes pattern |
| * replacements "{0}" for the distinguished name, and/or "{1}" for |
| * the username, of the authenticated user for which roles will be |
| * retrieved.</li> |
| * <li>The <code>roleBase</code> property can be set to the element that |
| * is the base of the search for matching roles. If not specified, |
| * the entire context will be searched.</li> |
| * <li>The <code>roleSubtree</code> property can be set to |
| * <code>true</code> if you wish to search the entire subtree of the |
| * directory context. The default value of <code>false</code> |
| * requests a search of only the current level.</li> |
| * <li>The element includes an attribute (whose name is configured by |
| * the <code>roleName</code> property) containing the name of the |
| * role represented by this element.</li> |
| * </ul></li> |
| * |
| * <li>In addition, roles may be represented by the values of an attribute |
| * in the user's element whose name is configured by the |
| * <code>userRoleName</code> property.</li> |
| * |
| * <li>Note that the standard <code><security-role-ref></code> element in |
| * the web application deployment descriptor allows applications to refer |
| * to roles programmatically by names other than those used in the |
| * directory server itself.</li> |
| * </ul> |
| * |
| * <p><strong>TODO</strong> - Support connection pooling (including message |
| * format objects) so that <code>authenticate()</code> does not have to be |
| * synchronized.</p> |
| * |
| * <p><strong>WARNING</strong> - There is a reported bug against the Netscape |
| * provider code (com.netscape.jndi.ldap.LdapContextFactory) with respect to |
| * successfully authenticated a non-existing user. The |
| * report is here: http://nagoya.apache.org/bugzilla/show_bug.cgi?id=11210 . |
| * With luck, Netscape has updated their provider code and this is not an |
| * issue. </p> |
| * |
| * @author John Holman |
| * @author Craig R. McClanahan |
| * @version $Revision$ $Date$ |
| */ |
| |
| public class JNDIRealm extends RealmBase { |
| |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| /** |
| * The type of authentication to use |
| */ |
| protected String authentication = null; |
| |
| /** |
| * The connection username for the server we will contact. |
| */ |
| protected String connectionName = null; |
| |
| |
| /** |
| * The connection password for the server we will contact. |
| */ |
| protected String connectionPassword = null; |
| |
| |
| /** |
| * The connection URL for the server we will contact. |
| */ |
| protected String connectionURL = null; |
| |
| |
| /** |
| * The directory context linking us to our directory server. |
| */ |
| protected DirContext context = null; |
| |
| |
| /** |
| * The JNDI context factory used to acquire our InitialContext. By |
| * default, assumes use of an LDAP server using the standard JNDI LDAP |
| * provider. |
| */ |
| protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; |
| |
| |
| /** |
| * Descriptive information about this Realm implementation. |
| */ |
| protected static final String info = |
| "org.apache.catalina.realm.JNDIRealm/1.0"; |
| |
| |
| /** |
| * Descriptive information about this Realm implementation. |
| */ |
| protected static final String name = "JNDIRealm"; |
| |
| |
| /** |
| * The protocol that will be used in the communication with the |
| * directory server. |
| */ |
| protected String protocol = null; |
| |
| |
| /** |
| * How should we handle referrals? Microsoft Active Directory can't handle |
| * the default case, so an application authenticating against AD must |
| * set referrals to "follow". |
| */ |
| protected String referrals = null; |
| |
| |
| /** |
| * The base element for user searches. |
| */ |
| protected String userBase = ""; |
| |
| |
| /** |
| * The message format used to search for a user, with "{0}" marking |
| * the spot where the username goes. |
| */ |
| protected String userSearch = null; |
| |
| |
| /** |
| * The MessageFormat object associated with the current |
| * <code>userSearch</code>. |
| */ |
| protected MessageFormat userSearchFormat = null; |
| |
| |
| /** |
| * Should we search the entire subtree for matching users? |
| */ |
| protected boolean userSubtree = false; |
| |
| |
| /** |
| * The attribute name used to retrieve the user password. |
| */ |
| protected String userPassword = null; |
| |
| |
| /** |
| * A string of LDAP user patterns or paths, ":"-separated |
| * These will be used to form the distinguished name of a |
| * user, with "{0}" marking the spot where the specified username |
| * goes. |
| * This is similar to userPattern, but allows for multiple searches |
| * for a user. |
| */ |
| protected String[] userPatternArray = null; |
| |
| |
| /** |
| * The message format used to form the distinguished name of a |
| * user, with "{0}" marking the spot where the specified username |
| * goes. |
| */ |
| protected String userPattern = null; |
| |
| |
| /** |
| * An array of MessageFormat objects associated with the current |
| * <code>userPatternArray</code>. |
| */ |
| protected MessageFormat[] userPatternFormatArray = null; |
| |
| |
| /** |
| * The base element for role searches. |
| */ |
| protected String roleBase = ""; |
| |
| |
| /** |
| * The MessageFormat object associated with the current |
| * <code>roleSearch</code>. |
| */ |
| protected MessageFormat roleFormat = null; |
| |
| |
| /** |
| * The name of an attribute in the user's entry containing |
| * roles for that user |
| */ |
| protected String userRoleName = null; |
| |
| |
| /** |
| * The name of the attribute containing roles held elsewhere |
| */ |
| protected String roleName = null; |
| |
| |
| /** |
| * The message format used to select roles for a user, with "{0}" marking |
| * the spot where the distinguished name of the user goes. |
| */ |
| protected String roleSearch = null; |
| |
| |
| /** |
| * Should we search the entire subtree for matching memberships? |
| */ |
| protected boolean roleSubtree = false; |
| |
| /** |
| * An alternate URL, to which, we should connect if connectionURL fails. |
| */ |
| protected String alternateURL; |
| |
| /** |
| * The number of connection attempts. If greater than zero we use the |
| * alternate url. |
| */ |
| protected int connectionAttempt = 0; |
| |
| /** |
| * The current user pattern to be used for lookup and binding of a user. |
| */ |
| protected int curUserPattern = 0; |
| |
| // ------------------------------------------------------------- Properties |
| |
| /** |
| * Return the type of authentication to use. |
| */ |
| public String getAuthentication() { |
| |
| return authentication; |
| |
| } |
| |
| /** |
| * Set the type of authentication to use. |
| * |
| * @param authentication The authentication |
| */ |
| public void setAuthentication(String authentication) { |
| |
| this.authentication = authentication; |
| |
| } |
| |
| /** |
| * Return the connection username for this Realm. |
| */ |
| public String getConnectionName() { |
| |
| return (this.connectionName); |
| |
| } |
| |
| |
| /** |
| * Set the connection username for this Realm. |
| * |
| * @param connectionName The new connection username |
| */ |
| public void setConnectionName(String connectionName) { |
| |
| this.connectionName = connectionName; |
| |
| } |
| |
| |
| /** |
| * Return the connection password for this Realm. |
| */ |
| public String getConnectionPassword() { |
| |
| return (this.connectionPassword); |
| |
| } |
| |
| |
| /** |
| * Set the connection password for this Realm. |
| * |
| * @param connectionPassword The new connection password |
| */ |
| public void setConnectionPassword(String connectionPassword) { |
| |
| this.connectionPassword = connectionPassword; |
| |
| } |
| |
| |
| /** |
| * Return the connection URL for this Realm. |
| */ |
| public String getConnectionURL() { |
| |
| return (this.connectionURL); |
| |
| } |
| |
| |
| /** |
| * Set the connection URL for this Realm. |
| * |
| * @param connectionURL The new connection URL |
| */ |
| public void setConnectionURL(String connectionURL) { |
| |
| this.connectionURL = connectionURL; |
| |
| } |
| |
| |
| /** |
| * Return the JNDI context factory for this Realm. |
| */ |
| public String getContextFactory() { |
| |
| return (this.contextFactory); |
| |
| } |
| |
| |
| /** |
| * Set the JNDI context factory for this Realm. |
| * |
| * @param contextFactory The new context factory |
| */ |
| public void setContextFactory(String contextFactory) { |
| |
| this.contextFactory = contextFactory; |
| |
| } |
| |
| |
| /** |
| * Return the protocol to be used. |
| */ |
| public String getProtocol() { |
| |
| return protocol; |
| |
| } |
| |
| /** |
| * Set the protocol for this Realm. |
| * |
| * @param protocol The new protocol. |
| */ |
| public void setProtocol(String protocol) { |
| |
| this.protocol = protocol; |
| |
| } |
| |
| |
| /** |
| * Returns the current settings for handling JNDI referrals. |
| */ |
| public String getReferrals () { |
| return referrals; |
| } |
| |
| |
| /** |
| * How do we handle JNDI referrals? ignore, follow, or throw |
| * (see javax.naming.Context.REFERRAL for more information). |
| */ |
| public void setReferrals (String referrals) { |
| this.referrals = referrals; |
| } |
| |
| |
| /** |
| * Return the base element for user searches. |
| */ |
| public String getUserBase() { |
| |
| return (this.userBase); |
| |
| } |
| |
| |
| /** |
| * Set the base element for user searches. |
| * |
| * @param userBase The new base element |
| */ |
| public void setUserBase(String userBase) { |
| |
| this.userBase = userBase; |
| |
| } |
| |
| |
| /** |
| * Return the message format pattern for selecting users in this Realm. |
| */ |
| public String getUserSearch() { |
| |
| return (this.userSearch); |
| |
| } |
| |
| |
| /** |
| * Set the message format pattern for selecting users in this Realm. |
| * |
| * @param userSearch The new user search pattern |
| */ |
| public void setUserSearch(String userSearch) { |
| |
| this.userSearch = userSearch; |
| if (userSearch == null) |
| userSearchFormat = null; |
| else |
| userSearchFormat = new MessageFormat(userSearch); |
| |
| } |
| |
| |
| /** |
| * Return the "search subtree for users" flag. |
| */ |
| public boolean getUserSubtree() { |
| |
| return (this.userSubtree); |
| |
| } |
| |
| |
| /** |
| * Set the "search subtree for users" flag. |
| * |
| * @param userSubtree The new search flag |
| */ |
| public void setUserSubtree(boolean userSubtree) { |
| |
| this.userSubtree = userSubtree; |
| |
| } |
| |
| |
| /** |
| * Return the user role name attribute name for this Realm. |
| */ |
| public String getUserRoleName() { |
| |
| return userRoleName; |
| } |
| |
| |
| /** |
| * Set the user role name attribute name for this Realm. |
| * |
| * @param userRoleName The new userRole name attribute name |
| */ |
| public void setUserRoleName(String userRoleName) { |
| |
| this.userRoleName = userRoleName; |
| |
| } |
| |
| |
| /** |
| * Return the base element for role searches. |
| */ |
| public String getRoleBase() { |
| |
| return (this.roleBase); |
| |
| } |
| |
| |
| /** |
| * Set the base element for role searches. |
| * |
| * @param roleBase The new base element |
| */ |
| public void setRoleBase(String roleBase) { |
| |
| this.roleBase = roleBase; |
| |
| } |
| |
| |
| /** |
| * Return the role name attribute name for this Realm. |
| */ |
| public String getRoleName() { |
| |
| return (this.roleName); |
| |
| } |
| |
| |
| /** |
| * Set the role name attribute name for this Realm. |
| * |
| * @param roleName The new role name attribute name |
| */ |
| public void setRoleName(String roleName) { |
| |
| this.roleName = roleName; |
| |
| } |
| |
| |
| /** |
| * Return the message format pattern for selecting roles in this Realm. |
| */ |
| public String getRoleSearch() { |
| |
| return (this.roleSearch); |
| |
| } |
| |
| |
| /** |
| * Set the message format pattern for selecting roles in this Realm. |
| * |
| * @param roleSearch The new role search pattern |
| */ |
| public void setRoleSearch(String roleSearch) { |
| |
| this.roleSearch = roleSearch; |
| if (roleSearch == null) |
| roleFormat = null; |
| else |
| roleFormat = new MessageFormat(roleSearch); |
| |
| } |
| |
| |
| /** |
| * Return the "search subtree for roles" flag. |
| */ |
| public boolean getRoleSubtree() { |
| |
| return (this.roleSubtree); |
| |
| } |
| |
| |
| /** |
| * Set the "search subtree for roles" flag. |
| * |
| * @param roleSubtree The new search flag |
| */ |
| public void setRoleSubtree(boolean roleSubtree) { |
| |
| this.roleSubtree = roleSubtree; |
| |
| } |
| |
| |
| /** |
| * Return the password attribute used to retrieve the user password. |
| */ |
| public String getUserPassword() { |
| |
| return (this.userPassword); |
| |
| } |
| |
| |
| /** |
| * Set the password attribute used to retrieve the user password. |
| * |
| * @param userPassword The new password attribute |
| */ |
| public void setUserPassword(String userPassword) { |
| |
| this.userPassword = userPassword; |
| |
| } |
| |
| |
| /** |
| * Return the message format pattern for selecting users in this Realm. |
| */ |
| public String getUserPattern() { |
| |
| return (this.userPattern); |
| |
| } |
| |
| |
| /** |
| * Set the message format pattern for selecting users in this Realm. |
| * This may be one simple pattern, or multiple patterns to be tried, |
| * separated by parentheses. (for example, either "cn={0}", or |
| * "(cn={0})(cn={0},o=myorg)" Full LDAP search strings are also supported, |
| * but only the "OR", "|" syntax, so "(|(cn={0})(cn={0},o=myorg))" is |
| * also valid. Complex search strings with &, etc are NOT supported. |
| * |
| * @param userPattern The new user pattern |
| */ |
| public void setUserPattern(String userPattern) { |
| |
| this.userPattern = userPattern; |
| if (userPattern == null) |
| userPatternArray = null; |
| else { |
| userPatternArray = parseUserPatternString(userPattern); |
| int len = this.userPatternArray.length; |
| userPatternFormatArray = new MessageFormat[len]; |
| for (int i=0; i < len; i++) { |
| userPatternFormatArray[i] = |
| new MessageFormat(userPatternArray[i]); |
| } |
| } |
| } |
| |
| |
| /** |
| * Getter for property alternateURL. |
| * |
| * @return Value of property alternateURL. |
| */ |
| public String getAlternateURL() { |
| |
| return this.alternateURL; |
| |
| } |
| |
| |
| /** |
| * Setter for property alternateURL. |
| * |
| * @param alternateURL New value of property alternateURL. |
| */ |
| public void setAlternateURL(String alternateURL) { |
| |
| this.alternateURL = alternateURL; |
| |
| } |
| |
| |
| // ---------------------------------------------------------- Realm Methods |
| |
| |
| /** |
| * Return the Principal associated with the specified username and |
| * credentials, if there is one; otherwise return <code>null</code>. |
| * |
| * If there are any errors with the JDBC connection, executing |
| * the query or anything we return null (don't authenticate). This |
| * event is also logged, and the connection will be closed so that |
| * a subsequent request will automatically re-open it. |
| * |
| * @param username Username of the Principal to look up |
| * @param credentials Password or other credentials to use in |
| * authenticating this username |
| */ |
| public Principal authenticate(String username, String credentials) { |
| |
| DirContext context = null; |
| Principal principal = null; |
| |
| try { |
| |
| // Ensure that we have a directory context available |
| context = open(); |
| |
| // Occassionally the directory context will timeout. Try one more |
| // time before giving up. |
| try { |
| |
| // Authenticate the specified username if possible |
| principal = authenticate(context, username, credentials); |
| |
| } catch (CommunicationException e) { |
| |
| |
| // If contains the work closed. Then assume socket is closed. |
| // If message is null, assume the worst and allow the |
| // connection to be closed. |
| if (e.getMessage()!=null && |
| e.getMessage().indexOf("closed") < 0) |
| throw(e); |
| |
| // log the exception so we know it's there. |
| container.getLogger().error(sm.getString("jndiRealm.exception"), e); |
| |
| // close the connection so we know it will be reopened. |
| if (context != null) |
| close(context); |
| |
| // open a new directory context. |
| context = open(); |
| |
| // Try the authentication again. |
| principal = authenticate(context, username, credentials); |
| |
| } |
| |
| |
| // Release this context |
| release(context); |
| |
| // Return the authenticated Principal (if any) |
| return (principal); |
| |
| } catch (NamingException e) { |
| |
| // Log the problem for posterity |
| container.getLogger().error(sm.getString("jndiRealm.exception"), e); |
| |
| // Close the connection so that it gets reopened next time |
| if (context != null) |
| close(context); |
| |
| // Return "not authenticated" for this request |
| return (null); |
| |
| } |
| |
| } |
| |
| |
| // -------------------------------------------------------- Package Methods |
| |
| |
| // ------------------------------------------------------ Protected Methods |
| |
| |
| /** |
| * Return the Principal associated with the specified username and |
| * credentials, if there is one; otherwise return <code>null</code>. |
| * |
| * @param context The directory context |
| * @param username Username of the Principal to look up |
| * @param credentials Password or other credentials to use in |
| * authenticating this username |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| public synchronized Principal authenticate(DirContext context, |
| String username, |
| String credentials) |
| throws NamingException { |
| |
| if (username == null || username.equals("") |
| || credentials == null || credentials.equals("")) |
| return (null); |
| |
| if (userPatternArray != null) { |
| for (curUserPattern = 0; |
| curUserPattern < userPatternFormatArray.length; |
| curUserPattern++) { |
| // Retrieve user information |
| User user = getUser(context, username); |
| if (user != null) { |
| try { |
| // Check the user's credentials |
| if (checkCredentials(context, user, credentials)) { |
| // Search for additional roles |
| List roles = getRoles(context, user); |
| return (new GenericPrincipal(this, |
| username, |
| credentials, |
| roles)); |
| } |
| } catch (InvalidNameException ine) { |
| // Log the problem for posterity |
| container.getLogger().warn(sm.getString("jndiRealm.exception"), ine); |
| // ignore; this is probably due to a name not fitting |
| // the search path format exactly, as in a fully- |
| // qualified name being munged into a search path |
| // that already contains cn= or vice-versa |
| } |
| } |
| } |
| return null; |
| } else { |
| // Retrieve user information |
| User user = getUser(context, username); |
| if (user == null) |
| return (null); |
| |
| // Check the user's credentials |
| if (!checkCredentials(context, user, credentials)) |
| return (null); |
| |
| // Search for additional roles |
| List roles = getRoles(context, user); |
| |
| // Create and return a suitable Principal for this user |
| return (new GenericPrincipal(this, username, credentials, roles)); |
| } |
| } |
| |
| |
| /** |
| * Return a User object containing information about the user |
| * with the specified username, if found in the directory; |
| * otherwise return <code>null</code>. |
| * |
| * If the <code>userPassword</code> configuration attribute is |
| * specified, the value of that attribute is retrieved from the |
| * user's directory entry. If the <code>userRoleName</code> |
| * configuration attribute is specified, all values of that |
| * attribute are retrieved from the directory entry. |
| * |
| * @param context The directory context |
| * @param username Username to be looked up |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| protected User getUser(DirContext context, String username) |
| throws NamingException { |
| |
| User user = null; |
| |
| // Get attributes to retrieve from user entry |
| ArrayList list = new ArrayList(); |
| if (userPassword != null) |
| list.add(userPassword); |
| if (userRoleName != null) |
| list.add(userRoleName); |
| String[] attrIds = new String[list.size()]; |
| list.toArray(attrIds); |
| |
| // Use pattern or search for user entry |
| if (userPatternFormatArray != null) { |
| user = getUserByPattern(context, username, attrIds); |
| } else { |
| user = getUserBySearch(context, username, attrIds); |
| } |
| |
| return user; |
| } |
| |
| |
| /** |
| * Use the <code>UserPattern</code> configuration attribute to |
| * locate the directory entry for the user with the specified |
| * username and return a User object; otherwise return |
| * <code>null</code>. |
| * |
| * @param context The directory context |
| * @param username The username |
| * @param attrIds String[]containing names of attributes to |
| * retrieve. |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| protected User getUserByPattern(DirContext context, |
| String username, |
| String[] attrIds) |
| throws NamingException { |
| |
| if (username == null || userPatternFormatArray[curUserPattern] == null) |
| return (null); |
| |
| // Form the dn from the user pattern |
| String dn = userPatternFormatArray[curUserPattern].format(new String[] { username }); |
| |
| // Return if no attributes to retrieve |
| if (attrIds == null || attrIds.length == 0) |
| return new User(username, dn, null, null); |
| |
| // Get required attributes from user entry |
| Attributes attrs = null; |
| try { |
| attrs = context.getAttributes(dn, attrIds); |
| } catch (NameNotFoundException e) { |
| return (null); |
| } |
| if (attrs == null) |
| return (null); |
| |
| // Retrieve value of userPassword |
| String password = null; |
| if (userPassword != null) |
| password = getAttributeValue(userPassword, attrs); |
| |
| // Retrieve values of userRoleName attribute |
| ArrayList roles = null; |
| if (userRoleName != null) |
| roles = addAttributeValues(userRoleName, attrs, roles); |
| |
| return new User(username, dn, password, roles); |
| } |
| |
| |
| /** |
| * Search the directory to return a User object containing |
| * information about the user with the specified username, if |
| * found in the directory; otherwise return <code>null</code>. |
| * |
| * @param context The directory context |
| * @param username The username |
| * @param attrIds String[]containing names of attributes to retrieve. |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| protected User getUserBySearch(DirContext context, |
| String username, |
| String[] attrIds) |
| throws NamingException { |
| |
| if (username == null || userSearchFormat == null) |
| return (null); |
| |
| // Form the search filter |
| String filter = userSearchFormat.format(new String[] { username }); |
| |
| // Set up the search controls |
| SearchControls constraints = new SearchControls(); |
| |
| if (userSubtree) { |
| constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); |
| } |
| else { |
| constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE); |
| } |
| |
| // Specify the attributes to be retrieved |
| if (attrIds == null) |
| attrIds = new String[0]; |
| constraints.setReturningAttributes(attrIds); |
| |
| NamingEnumeration results = |
| context.search(userBase, filter, constraints); |
| |
| |
| // Fail if no entries found |
| if (results == null || !results.hasMore()) { |
| return (null); |
| } |
| |
| // Get result for the first entry found |
| SearchResult result = (SearchResult)results.next(); |
| |
| // Check no further entries were found |
| if (results.hasMore()) { |
| container.getLogger().info("username " + username + " has multiple entries"); |
| return (null); |
| } |
| |
| // Get the entry's distinguished name |
| NameParser parser = context.getNameParser(""); |
| Name contextName = parser.parse(context.getNameInNamespace()); |
| Name baseName = parser.parse(userBase); |
| Name entryName = parser.parse(result.getName()); |
| Name name = contextName.addAll(baseName); |
| name = name.addAll(entryName); |
| String dn = name.toString(); |
| |
| if (container.getLogger().isTraceEnabled()) |
| container.getLogger().trace(" entry found for " + username + " with dn " + dn); |
| |
| // Get the entry's attributes |
| Attributes attrs = result.getAttributes(); |
| if (attrs == null) |
| return null; |
| |
| // Retrieve value of userPassword |
| String password = null; |
| if (userPassword != null) |
| password = getAttributeValue(userPassword, attrs); |
| |
| // Retrieve values of userRoleName attribute |
| ArrayList roles = null; |
| if (userRoleName != null) |
| roles = addAttributeValues(userRoleName, attrs, roles); |
| |
| return new User(username, dn, password, roles); |
| } |
| |
| |
| /** |
| * Check whether the given User can be authenticated with the |
| * given credentials. If the <code>userPassword</code> |
| * configuration attribute is specified, the credentials |
| * previously retrieved from the directory are compared explicitly |
| * with those presented by the user. Otherwise the presented |
| * credentials are checked by binding to the directory as the |
| * user. |
| * |
| * @param context The directory context |
| * @param user The User to be authenticated |
| * @param credentials The credentials presented by the user |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| protected boolean checkCredentials(DirContext context, |
| User user, |
| String credentials) |
| throws NamingException { |
| |
| boolean validated = false; |
| |
| if (userPassword == null) { |
| validated = bindAsUser(context, user, credentials); |
| } else { |
| validated = compareCredentials(context, user, credentials); |
| } |
| |
| if (container.getLogger().isTraceEnabled()) { |
| if (validated) { |
| container.getLogger().trace(sm.getString("jndiRealm.authenticateSuccess", |
| user.username)); |
| } else { |
| container.getLogger().trace(sm.getString("jndiRealm.authenticateFailure", |
| user.username)); |
| } |
| } |
| return (validated); |
| } |
| |
| |
| |
| /** |
| * Check whether the credentials presented by the user match those |
| * retrieved from the directory. |
| * |
| * @param context The directory context |
| * @param info The User to be authenticated |
| * @param credentials Authentication credentials |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| protected boolean compareCredentials(DirContext context, |
| User info, |
| String credentials) |
| throws NamingException { |
| |
| if (info == null || credentials == null) |
| return (false); |
| |
| String password = info.password; |
| if (password == null) |
| return (false); |
| |
| // Validate the credentials specified by the user |
| if (container.getLogger().isTraceEnabled()) |
| container.getLogger().trace(" validating credentials"); |
| |
| boolean validated = false; |
| if (hasMessageDigest()) { |
| // iPlanet support if the values starts with {SHA1} |
| // The string is in a format compatible with Base64.encode not |
| // the Hex encoding of the parent class. |
| if (password.startsWith("{SHA}")) { |
| /* sync since super.digest() does this same thing */ |
| synchronized (this) { |
| password = password.substring(5); |
| md.reset(); |
| md.update(credentials.getBytes()); |
| String digestedPassword = |
| new String(Base64.encode(md.digest())); |
| validated = password.equals(digestedPassword); |
| } |
| } else { |
| // Hex hashes should be compared case-insensitive |
| validated = (digest(credentials).equalsIgnoreCase(password)); |
| } |
| } else |
| validated = (digest(credentials).equals(password)); |
| return (validated); |
| |
| } |
| |
| |
| |
| /** |
| * Check credentials by binding to the directory as the user |
| * |
| * @param context The directory context |
| * @param user The User to be authenticated |
| * @param credentials Authentication credentials |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| protected boolean bindAsUser(DirContext context, |
| User user, |
| String credentials) |
| throws NamingException { |
| Attributes attr; |
| |
| if (credentials == null || user == null) |
| return (false); |
| |
| String dn = user.dn; |
| if (dn == null) |
| return (false); |
| |
| // Validate the credentials specified by the user |
| if (container.getLogger().isTraceEnabled()) { |
| container.getLogger().trace(" validating credentials by binding as the user"); |
| } |
| |
| // Set up security environment to bind as the user |
| context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); |
| context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); |
| |
| // Elicit an LDAP bind operation |
| boolean validated = false; |
| try { |
| if (container.getLogger().isTraceEnabled()) { |
| container.getLogger().trace(" binding as " + dn); |
| } |
| attr = context.getAttributes("", null); |
| validated = true; |
| } |
| catch (AuthenticationException e) { |
| if (container.getLogger().isTraceEnabled()) { |
| container.getLogger().trace(" bind attempt failed"); |
| } |
| } |
| |
| // Restore the original security environment |
| if (connectionName != null) { |
| context.addToEnvironment(Context.SECURITY_PRINCIPAL, |
| connectionName); |
| } else { |
| context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); |
| } |
| |
| if (connectionPassword != null) { |
| context.addToEnvironment(Context.SECURITY_CREDENTIALS, |
| connectionPassword); |
| } |
| else { |
| context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); |
| } |
| |
| return (validated); |
| } |
| |
| |
| /** |
| * Return a List of roles associated with the given User. Any |
| * roles present in the user's directory entry are supplemented by |
| * a directory search. If no roles are associated with this user, |
| * a zero-length List is returned. |
| * |
| * @param context The directory context we are searching |
| * @param user The User to be checked |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| protected List getRoles(DirContext context, User user) |
| throws NamingException { |
| |
| if (user == null) |
| return (null); |
| |
| String dn = user.dn; |
| String username = user.username; |
| |
| if (dn == null || username == null) |
| return (null); |
| |
| if (container.getLogger().isTraceEnabled()) |
| container.getLogger().trace(" getRoles(" + dn + ")"); |
| |
| // Start with roles retrieved from the user entry |
| ArrayList list = user.roles; |
| if (list == null) { |
| list = new ArrayList(); |
| } |
| |
| // Are we configured to do role searches? |
| if ((roleFormat == null) || (roleName == null)) |
| return (list); |
| |
| // Set up parameters for an appropriate search |
| String filter = roleFormat.format(new String[] { doRFC2254Encoding(dn), username }); |
| SearchControls controls = new SearchControls(); |
| if (roleSubtree) |
| controls.setSearchScope(SearchControls.SUBTREE_SCOPE); |
| else |
| controls.setSearchScope(SearchControls.ONELEVEL_SCOPE); |
| controls.setReturningAttributes(new String[] {roleName}); |
| |
| // Perform the configured search and process the results |
| NamingEnumeration results = |
| context.search(roleBase, filter, controls); |
| if (results == null) |
| return (list); // Should never happen, but just in case ... |
| while (results.hasMore()) { |
| SearchResult result = (SearchResult) results.next(); |
| Attributes attrs = result.getAttributes(); |
| if (attrs == null) |
| continue; |
| list = addAttributeValues(roleName, attrs, list); |
| } |
| |
| |
| if (container.getLogger().isTraceEnabled()) { |
| if (list != null) { |
| container.getLogger().trace(" Returning " + list.size() + " roles"); |
| for (int i=0; i<list.size(); i++) |
| container.getLogger().trace( " Found role " + list.get(i)); |
| } else { |
| container.getLogger().trace(" getRoles about to return null "); |
| } |
| } |
| |
| return (list); |
| } |
| |
| |
| /** |
| * Return a String representing the value of the specified attribute. |
| * |
| * @param attrId Attribute name |
| * @param attrs Attributes containing the required value |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| private String getAttributeValue(String attrId, Attributes attrs) |
| throws NamingException { |
| |
| if (container.getLogger().isTraceEnabled()) |
| container.getLogger().trace(" retrieving attribute " + attrId); |
| |
| if (attrId == null || attrs == null) |
| return null; |
| |
| Attribute attr = attrs.get(attrId); |
| if (attr == null) |
| return (null); |
| Object value = attr.get(); |
| if (value == null) |
| return (null); |
| String valueString = null; |
| if (value instanceof byte[]) |
| valueString = new String((byte[]) value); |
| else |
| valueString = value.toString(); |
| |
| return valueString; |
| } |
| |
| |
| |
| /** |
| * Add values of a specified attribute to a list |
| * |
| * @param attrId Attribute name |
| * @param attrs Attributes containing the new values |
| * @param values ArrayList containing values found so far |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| private ArrayList addAttributeValues(String attrId, |
| Attributes attrs, |
| ArrayList values) |
| throws NamingException{ |
| |
| if (container.getLogger().isTraceEnabled()) |
| container.getLogger().trace(" retrieving values for attribute " + attrId); |
| if (attrId == null || attrs == null) |
| return values; |
| if (values == null) |
| values = new ArrayList(); |
| Attribute attr = attrs.get(attrId); |
| if (attr == null) |
| return (values); |
| NamingEnumeration e = attr.getAll(); |
| while(e.hasMore()) { |
| String value = (String)e.next(); |
| values.add(value); |
| } |
| return values; |
| } |
| |
| |
| /** |
| * Close any open connection to the directory server for this Realm. |
| * |
| * @param context The directory context to be closed |
| */ |
| protected void close(DirContext context) { |
| |
| // Do nothing if there is no opened connection |
| if (context == null) |
| return; |
| |
| // Close our opened connection |
| try { |
| if (container.getLogger().isDebugEnabled()) |
| container.getLogger().debug("Closing directory context"); |
| context.close(); |
| } catch (NamingException e) { |
| container.getLogger().error(sm.getString("jndiRealm.close"), e); |
| } |
| this.context = null; |
| |
| } |
| |
| |
| /** |
| * Return a short name for this Realm implementation. |
| */ |
| protected String getName() { |
| |
| return (name); |
| |
| } |
| |
| |
| /** |
| * Return the password associated with the given principal's user name. |
| */ |
| protected String getPassword(String username) { |
| |
| return (null); |
| |
| } |
| |
| |
| /** |
| * Return the Principal associated with the given user name. |
| */ |
| protected Principal getPrincipal(String username) { |
| |
| return (null); |
| |
| } |
| |
| |
| |
| /** |
| * Open (if necessary) and return a connection to the configured |
| * directory server for this Realm. |
| * |
| * @exception NamingException if a directory server error occurs |
| */ |
| protected DirContext open() throws NamingException { |
| |
| // Do nothing if there is a directory server connection already open |
| if (context != null) |
| return (context); |
| |
| try { |
| |
| // Ensure that we have a directory context available |
| context = new InitialDirContext(getDirectoryContextEnvironment()); |
| |
| } catch (Exception e) { |
| |
| connectionAttempt = 1; |
| |
| // log the first exception. |
| container.getLogger().warn(sm.getString("jndiRealm.exception"), e); |
| |
| // Try connecting to the alternate url. |
| context = new InitialDirContext(getDirectoryContextEnvironment()); |
| |
| } finally { |
| |
| // reset it in case the connection times out. |
| // the primary may come back. |
| connectionAttempt = 0; |
| |
| } |
| |
| return (context); |
| |
| } |
| |
| /** |
| * Create our directory context configuration. |
| * |
| * @return java.util.Hashtable the configuration for the directory context. |
| */ |
| protected Hashtable getDirectoryContextEnvironment() { |
| |
| Hashtable env = new Hashtable(); |
| |
| // Configure our directory context environment. |
| if (container.getLogger().isDebugEnabled() && connectionAttempt == 0) |
| container.getLogger().debug("Connecting to URL " + connectionURL); |
| else if (container.getLogger().isDebugEnabled() && connectionAttempt > 0) |
| container.getLogger().debug("Connecting to URL " + alternateURL); |
| env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory); |
| if (connectionName != null) |
| env.put(Context.SECURITY_PRINCIPAL, connectionName); |
| if (connectionPassword != null) |
| env.put(Context.SECURITY_CREDENTIALS, connectionPassword); |
| if (connectionURL != null && connectionAttempt == 0) |
| env.put(Context.PROVIDER_URL, connectionURL); |
| else if (alternateURL != null && connectionAttempt > 0) |
| env.put(Context.PROVIDER_URL, alternateURL); |
| if (authentication != null) |
| env.put(Context.SECURITY_AUTHENTICATION, authentication); |
| if (protocol != null) |
| env.put(Context.SECURITY_PROTOCOL, protocol); |
| if (referrals != null) |
| env.put(Context.REFERRAL, referrals); |
| |
| return env; |
| |
| } |
| |
| |
| /** |
| * Release our use of this connection so that it can be recycled. |
| * |
| * @param context The directory context to release |
| */ |
| protected void release(DirContext context) { |
| |
| ; // NO-OP since we are not pooling anything |
| |
| } |
| |
| |
| // ------------------------------------------------------ Lifecycle Methods |
| |
| |
| /** |
| * Prepare for active use of the public methods of this Component. |
| * |
| * @exception LifecycleException if this component detects a fatal error |
| * that prevents it from being started |
| */ |
| public void start() throws LifecycleException { |
| |
| // Validate that we can open our connection |
| try { |
| open(); |
| } catch (NamingException e) { |
| throw new LifecycleException(sm.getString("jndiRealm.open"), e); |
| } |
| |
| // Perform normal superclass initialization |
| super.start(); |
| |
| } |
| |
| |
| /** |
| * Gracefully shut down active use of the public methods of this Component. |
| * |
| * @exception LifecycleException if this component detects a fatal error |
| * that needs to be reported |
| */ |
| public void stop() throws LifecycleException { |
| |
| // Perform normal superclass finalization |
| super.stop(); |
| |
| // Close any open directory server connection |
| close(this.context); |
| |
| } |
| |
| /** |
| * Given a string containing LDAP patterns for user locations (separated by |
| * parentheses in a pseudo-LDAP search string format - |
| * "(location1)(location2)", returns an array of those paths. Real LDAP |
| * search strings are supported as well (though only the "|" "OR" type). |
| * |
| * @param userPatternString - a string LDAP search paths surrounded by |
| * parentheses |
| */ |
| protected String[] parseUserPatternString(String userPatternString) { |
| |
| if (userPatternString != null) { |
| ArrayList pathList = new ArrayList(); |
| int startParenLoc = userPatternString.indexOf('('); |
| if (startParenLoc == -1) { |
| // no parens here; return whole thing |
| return new String[] {userPatternString}; |
| } |
| int startingPoint = 0; |
| while (startParenLoc > -1) { |
| int endParenLoc = 0; |
| // weed out escaped open parens and parens enclosing the |
| // whole statement (in the case of valid LDAP search |
| // strings: (|(something)(somethingelse)) |
| while ( (userPatternString.charAt(startParenLoc + 1) == '|') || |
| (startParenLoc != 0 && userPatternString.charAt(startParenLoc - 1) == '\\') ) { |
| startParenLoc = userPatternString.indexOf("(", startParenLoc+1); |
| } |
| endParenLoc = userPatternString.indexOf(")", startParenLoc+1); |
| // weed out escaped end-parens |
| while (userPatternString.charAt(endParenLoc - 1) == '\\') { |
| endParenLoc = userPatternString.indexOf(")", endParenLoc+1); |
| } |
| String nextPathPart = userPatternString.substring |
| (startParenLoc+1, endParenLoc); |
| pathList.add(nextPathPart); |
| startingPoint = endParenLoc+1; |
| startParenLoc = userPatternString.indexOf('(', startingPoint); |
| } |
| return (String[])pathList.toArray(new String[] {}); |
| } |
| return null; |
| |
| } |
| |
| |
| /** |
| * Given an LDAP search string, returns the string with certain characters |
| * escaped according to RFC 2254 guidelines. |
| * The character mapping is as follows: |
| * char -> Replacement |
| * --------------------------- |
| * * -> \2a |
| * ( -> \28 |
| * ) -> \29 |
| * \ -> \5c |
| * \0 -> \00 |
| * @param inString string to escape according to RFC 2254 guidelines |
| * @return String the escaped/encoded result |
| */ |
| protected String doRFC2254Encoding(String inString) { |
| StringBuffer buf = new StringBuffer(inString.length()); |
| for (int i = 0; i < inString.length(); i++) { |
| char c = inString.charAt(i); |
| switch (c) { |
| case '\\': |
| buf.append("\\5c"); |
| break; |
| case '*': |
| buf.append("\\2a"); |
| break; |
| case '(': |
| buf.append("\\28"); |
| break; |
| case ')': |
| buf.append("\\29"); |
| break; |
| case '\0': |
| buf.append("\\00"); |
| break; |
| default: |
| buf.append(c); |
| break; |
| } |
| } |
| return buf.toString(); |
| } |
| |
| |
| } |
| |
| // ------------------------------------------------------ Private Classes |
| |
| /** |
| * A private class representing a User |
| */ |
| class User { |
| String username = null; |
| String dn = null; |
| String password = null; |
| ArrayList roles = null; |
| |
| |
| User(String username, String dn, String password, ArrayList roles) { |
| this.username = username; |
| this.dn = dn; |
| this.password = password; |
| this.roles = roles; |
| } |
| |
| } |